Last active
April 2, 2023 22:59
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' } | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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).