Tutorial: Incrementing a Counter
To demonstrate building out dynamic API routes and then progressively enhancing them we’ll build an incrementing counter. First rough in the HTML.
Part 1: basic HTML form handling
- Create a new Enhance app
npx "@enhance/cli@latest" new ./counter-app
. This generates a starter project withapp/pages/index.html
, and static assets inpublic/
. - Create a form for incrementing at
app/elements/form-counter.mjs
:
export default function counter ({ html, state }) {
return html`
<form action=/count method=post>
<button>+1</button>
</form>
<pre>${JSON.stringify(state, null, 2)}</pre>
`
}
Note the handy <pre>
debugger on line 6!
And add the new custom element to app/pages/index.html
:
<form-counter></form-counter>
- Create an API route to read the current count at
app/api/index.mjs
with the following contents:
export async function get (req) {
const count = req.session.count || 0
return {
json: { count }
}
}
- Create an API route to handle
POST /count
by creating a fileapp/api/count.mjs
with the following:
export async function post (req) {
let count = req.session.count || 0
count += 1
return {
session: { count },
location: '/'
}
}
The function above reads the count from the session or sets a default value, increments count
, and then writes the session, and redirects to /
. Always redirect after a form post to prevent double form submission, and proper back-button behavior.
- Preview the counter by running
npm start
; you’ll see thestate
update in the handy<pre>
tag debugger we created in step 1.
Part 2: progressive enhancement
The form functions even when client JS isn’t available. This is the moment where we can improve the web consumer experience, and augment the form to work without a posting the form, and incurring a server round trip.
- Add a JSON result to
app/api/count.mjs
export async function post (req) {
let count = req.session.count || 0
count += 1
return {
session: { count },
json: { count },
location: '/'
}
}
Now anytime POST /count
receives a request for JSON it will get { count }
.
Create a completely vanilla JS upgrade for the custom element:
export class Counter extends HTMLElement {
constructor () {
super()
this.form = this.querySelector('form')
this.pre = this.querySelector('pre')
this.form.addEventListener('submit', this.addOne.bind(this))
}
async addOne (e) {
e.preventDefault()
const res = await fetch('/count', {
headers: { 'accept': 'application/json' },
method: 'post'
})
const json = await res.json()
this.pre.innerHTML = JSON.stringify(json, null, 2)
}
}
customElements.define('form-counter', Counter)
Any framework or library could be used but this example is to show those are optional; the nice thing about working with the low level code is there are no dependencies and this will work in all browsers forevermore.
Add the client script to the custom element, and reload to see the enhanced version.
export default function counter ({ html, state }) {
return html`
<form action=/count method=post>
<button>+1</button>
</form>
<pre>${JSON.stringify(state, null, 2)}</pre>
<script type=module src=/_public/form-counter.mjs></script>
`
}