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](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.
Top comments (0)