On the eighth day of Enhancing: Progressively Enhanced Forms
by Simon MacDonald
@macdonst
@macdonst@mastodon.online
on
Original photo by Mehrshad Rajabi on Unsplash
Day 7 introduced a bunch of new code into our application for handling forms. Today we will make a new progressively enhanced submit button that makes a fetch call to our API when JavaScript is available and uses a form post when it’s not.
Create a Submit Button
It might be second nature to you by now, but let’s create a new element for our progressively enhanced submit button.
begin gen element --name submit-button-pe
Enhance form elements gives us a pretty good starting point for our submit button. Let’s copy the default submit-button
code into our app/elements/submit-button-pe.mjs
file:
export default function Element({ html }) {
return html`
<style>
:host button {
color: var(--light);
background-color: var(--primary-500)
}
:host button:focus, :host button:hover {
outline: none;
background-color: var(--primary-400)
}
</style>
<button class="whitespace-no-wrap pb-3 pt-3 pl0 pr0 font-medium text0 cursor-pointer radius0">
<slot name="label"></slot>
</button>
`
}
Then edit the app/pages/comments.mjs
file so we use submit-button-pe
instead of enhance-submit-button
on line 44. It should look like this:
<submit-button-pe style="float: right"><span slot="label">Save</span></submit-button-pe>
When you reload http://localhost:3333/comments
you won’t notice any changes but we are now setup to progressively enhance this button.
Enhance the Submit Button
We’ve gotten to day 8 of our series and haven’t yet written a line of client-side JavaScript code but that’s about to change. Instead of having our submit button do a form post to the /comments
endpoint, we will use fetch
on the browser to submit the new comment. Then we’ll update the DOM with the newly created comment.
We’ll add a script
tag to our app/elements/submit-button-pe.mjs
to contain the client-side JavaScript. This is where things will start to look more like a plain vanilla web component. Right after the close button
tag add the following script tag.
<script>
class SubmitButton extends HTMLElement {
}
customElements.define('submit-button-pe', SubmitButton)
</script>
This is all we need to register the submit-button-pe
component with the browser at runtime.
Then we’ll add a constructor
method in our SubmitButton
class:
constructor () {
super()
this.submitForm = this.submitForm.bind(this)
this.addEventListener('click', this.submitForm)
}
In our constructor
, we call super
to run the constructor in HTMLElement
then we bind the submitForm
method (we’ll write that next) to the current object and add a click
listener to call submitForm
whenever the button is clicked.
Now we’ll create the submitForm
method.
submitForm (e) {
if ("fetch" in window) {
e.preventDefault()
let form = this.closest('form')
let body = JSON.stringify(Object.fromEntries(new FormData(form)))
fetch(form.action, {
method: form.method,
body,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
})
.then(response => response.json())
.then(data => {
const main = document.querySelector('main')
const details = document.querySelector('details')
let article = document.createElement('article')
article.innerHTML = this.createArticle(data)
main.insertBefore(article, details)
})
.catch(error => {
console.log(error)
})
}
}
This function does several interesting things, so let’s enumerate them:
- We check to see if
fetch
is supported. If not, the component will continue to act like a regular button, and a form post will occur. - If
fetch
is supported we prevent the default behavior from happening as we don’t want to double-submit this comment. - We walk the DOM to find the closest
form
element. - We create a stringified JSON object from the form’s data.
- Then, we use
fetch
to post the data to our/comments
endpoint. - On a successful response, we convert it to JSON
- And then, we insert a new
article
into the DOM.
All of this happens without a page load in browsers that support fetch
. However, if you are running an older browser or something goes wrong with JavaScript the application will still work because it is built HTML first.
Full source code of the element
export default function Element({ html }) {
return html`
<style>
:host button {
color: var(--light);
background-color: var(--primary-500)
}
:host button:focus, :host button:hover {
outline: none;
background-color: var(--primary-400)
}
</style>
<button class="whitespace-no-wrap pb-3 pt-3 pl0 pr0 font-medium text0 cursor-pointer radius0">
<slot name="label"></slot>
</button>
<script>
class SubmitButton extends HTMLElement {
constructor () {
super()
this.submitForm = this.submitForm.bind(this)
this.addEventListener('click', this.submitForm)
}
submitForm (e) {
if ("fetch" in window) {
e.preventDefault()
let form = this.closest('form')
let body = JSON.stringify(Object.fromEntries(new FormData(form)))
fetch(form.action, {
method: form.method,
body,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
})
.then(response => response.json())
.then(data => {
const main = document.querySelector('main')
const details = document.querySelector('details')
let article = document.createElement('article')
article.innerHTML = this.createArticle(data)
main.insertBefore(article, details)
})
.catch(error => {
console.log(error)
})
}
}
createArticle({comment}) {
return \`<div class="mb0">
<p class="pb-2"><strong class="capitalize">name: </strong>\${comment.name}</p>
<p class="pb-2"><strong class="capitalize">email: </strong>s\${comment.email}</p>
<p class="pb-2"><strong class="capitalize">subject: </strong>\${comment.subject}</p>
<p class="pb-2"><strong class="capitalize">message: </strong>\${comment.message}</p>
<p class="pb-2"><strong class="capitalize">key: </strong>\${comment.key}</p>
</div>
<p class="mb-1">
<link-element href="/comments/\${comment.key}">
<a href="/comments/\${comment.key}">
Edit this comment
</a></link-element>
</p>
<form action="/comments/\${comment.key}/delete" method="POST" class="mb-1">
<submit-button>
<button class="whitespace-no-wrap pb-3 pt-3 pl0 pr0 font-medium text0 cursor-pointer radius0"><span slot="label">Delete this comment</span></button>
</submit-button>
</form>\`
}
}
customElements.define('submit-button-pe', SubmitButton)
</script>
`
}
Next Steps
Tomorrow, we’ll move on to part two of our progressive enhancement. While it is nice to have a single file component containing all of our HTML, CSS and JavaScript, it gets to be a bit difficult writing complex JavaScript in a tag template literal, even with editor extensions for syntax highlighting.