Skip to content

Instantly share code, notes, and snippets.

@jthoward64
Last active April 2, 2023 22:59
Show Gist options
  • Save jthoward64/7c7b11cbe8997ddd7fa47506f68938ff to your computer and use it in GitHub Desktop.
Save jthoward64/7c7b11cbe8997ddd7fa47506f68938ff to your computer and use it in GitHub Desktop.
A web component replacement for "form" that uses fetch to avoid redirecting the browser upon form submission.
<form action="/url" method="post" id="xhr-form" is="xhr-form">
<h2>XHR POST Example</h2>
<input type="text" name="name" placeholder="Name">
<input type="number" name="age" placeholder="Age">
<input type="submit" value="Submit">
</form>
<script>
const xhrForm = document.getElementById('xhr-form');
xhrForm.addEventListener('xhr-form-success', (event) => {
console.log('XHR Form Success', event.detail);
});
xhrForm.addEventListener('xhr-form-failure', (event) => {
console.log('XHR Form Failure', event.detail);
});
</script>
function urlencodeFormData(fd: FormData) {
let s = '';
function encode(s: string) {
return encodeURIComponent(s).replace(/%20/g, '+');
}
const formData: [string, string][] = [];
fd.forEach((value, key) => {
if (value instanceof File) {
formData.push([key, value.name]);
} else {
formData.push([key, value]);
}
});
for (const [key, value] of formData) {
s += (s ? '&' : '') + encode(key) + '=' + encode(value);
}
return s;
}
const xhrOnSubmit = (event: SubmitEvent) => {
console.log('Form submitted');
const form: HTMLFormElement | null =
event.target instanceof HTMLFormElement ? event.target : null;
if (form == null) {
console.error('Event target of form listener is not a form!');
return;
}
let baseUrl = form.action;
if (baseUrl == null || baseUrl === '') {
baseUrl = window.location.href;
}
const requestUrl = new URL(baseUrl, window.location.href);
const shouldClear = form.getAttribute('data-clear-form') === 'true';
// Decide on encoding
const formenctype =
event.submitter?.getAttribute('formenctype') ??
event.submitter?.getAttribute('formencoding');
const enctype =
formenctype ??
form.getAttribute('enctype') ??
form.getAttribute('encoding') ??
'application/x-www-form-urlencoded';
// Decide on method
let formMethod =
event.submitter?.getAttribute('formmethod') ??
form.getAttribute('method')?.toLowerCase() ??
'get';
const formData = new FormData(form);
// Encode body
let body: BodyInit | null = null;
if (formMethod === 'get') {
requestUrl.search = new URLSearchParams(
urlencodeFormData(formData)
).toString();
} else if (formMethod === 'post') {
if (enctype === 'application/x-www-form-urlencoded') {
body = urlencodeFormData(formData);
} else if (enctype === 'multipart/form-data') {
body = formData;
} else if (enctype === 'text/plain') {
let text = '';
// @ts-ignore - FormData.entries() is not in the TS definition
for (const element of formData.keys()) {
text += `${element}=${JSON.stringify(formData.get(element))}\n`;
}
} else {
throw new Error(`Illegal enctype: ${enctype}`);
}
} else if (formMethod === 'dialog') {
// Allow default behavior
return;
} else {
throw new Error(`Illegal form method: ${formMethod}`);
}
// Send request
const requestOptions: RequestInit = {
method: formMethod,
headers: {
'Content-Type': enctype,
},
};
if (body != null && formMethod === 'post') {
requestOptions.body = body;
}
const response = fetch(baseUrl, requestOptions).then((response) => {
if (shouldClear) {
form.reset();
}
if (response.ok) {
form.dispatchEvent(
new CustomEvent('xhr-form-success', {
detail: response,
})
);
} else {
form.dispatchEvent(
new CustomEvent('xhr-form-failure', {
detail: response,
})
);
}
return response;
});
event.preventDefault();
};
customElements.define(
'xhr-form',
class extends HTMLFormElement {
constructor() {
console.log('Form constructed');
super();
}
connectedCallback() {
this.addEventListener('submit', xhrOnSubmit);
}
disconnectedCallback() {
this.removeEventListener('submit', xhrOnSubmit);
}
},
{ extends: 'form' }
);
@jthoward64
Copy link
Author

An answer to this SO question. This web component closely emulates the behavior of the native form element without requiring any shadow DOM nonsense. Essentially it just replaces the default submit behavior (except for dialog forms) with the fetch API (could be easily adapted for XMLHttpRequest).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment