On the ninth day of Enhancing: Externalizing Scripts
by Simon MacDonald
@macdonst
@macdonst@mastodon.online
on
Original photo by Ardian Lumi on Unsplash
Yesterday, we progressively enhanced our submit button to enable a better user experience when JavaScript is enabled. Unfortunately, the more complicated your single-file component becomes, the worse the developer experience becomes, as it’s hard to write JavaScript in a tagged template literal, even with modern developer tools. Today we’ll break this single-file component into separate files for a better developer experience.
Externalizing the script tag
First, create a new file public/submit-button-pe.mjs
. Then cut the contents of the script
tag from app/elements/submit-button-pe.mjs
and paste it into public/submit-button-pe.mjs
. You will also need to add export default
to the class definition. Here’s what the source would look like:
export default 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)
Then update the script
tag in app/elements/submit-button-pe.mjs
to point to our externalized JavaScript 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>
<script type="module" src="/_public/submit-button-pe.mjs"></script>
`
}
Play around with http://localhost:3333/comments you won’t notice any changes in the browser, but your developer experience will be way better now that you are not editing JavaScript in a tagged template literal.
What about that ugly createArticle
method?
Oh right, I was hoping you wouldn’t notice that, but you are too smart for me. Let’s clean that up by extracting it into it’s own web component.
Create a new file public/comment-article.mjs
and populate it with the following code;
export default class CommentArticle extends HTMLElement {
connectedCallback() {
const name = this.getAttribute('name') || ''
const email = this.getAttribute('email') || ''
const subject = this.getAttribute('subject') || ''
const message = this.getAttribute('message') || ''
const key = this.getAttribute('key') || ''
this.innerHTML = `<div class="mb0">
<p class="pb-2"><strong class="capitalize">name: </strong>${name}</p>
<p class="pb-2"><strong class="capitalize">email: </strong>${email}</p>
<p class="pb-2"><strong class="capitalize">subject: </strong>${subject}</p>
<p class="pb-2"><strong class="capitalize">message: </strong>${message}</p>
<p class="pb-2"><strong class="capitalize">key: </strong>${key}</p>
</div>
<p class="mb-1">
<link-element href="/comments/${key}">
<a href="/comments/${key}">
Edit this comment
</a></link-element>
</p>
<form action="/comments/${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('comment-article', CommentArticle)
Creating the UI is still ugly but now it’s divorced from our submit-button-pe
. Over in app/elements/submit-button-pe.mjs
we’ll add another script
tag so we can use this component. Right above the existing script
tag add the line:
<script type="module" src="/_public/comment-article.mjs"></script>
Finally we’ll update public/submit-button-pe.mjs
to use the create-article
component instead of the createArticle
method. We’ll delete the createArticle
method and then instead of setting the innerHTML
of the article
we’ll create a create-article
component and set it’s attributes programmatically.
export default 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(({comment}) => {
const main = document.querySelector('main')
const details = document.querySelector('details')
let article = document.createElement('comment-article')
article.setAttribute('name', comment.name)
article.setAttribute('email', comment.email)
article.setAttribute('subject', comment.subject)
article.setAttribute('message', comment.message)
article.setAttribute('key', comment.key)
main.insertBefore(article, details)
})
.catch(error => {
console.log(error)
})
}
}
}
customElements.define('submit-button-pe', SubmitButton)
Once again, no changes for the user but it cleans up our code quite a bit. Is that exactly how I’d leave that comment-article
component? Well no, but it’s good enough for right now.
Next Steps
Tomorrow we’ll do the first deployment of our app.