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

  1. Create a new Enhance app npx "@enhance/cli@latest" new ./counter-app. This generates a starter project with app/pages/index.html, and static assets in public/.
  2. 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>
  1. 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 }
  }
}
  1. Create an API route to handle POST /count by creating a file app/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.

  1. Preview the counter by running npm start; you’ll see the state 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.

  1. 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>
  `
}

Community Resources

GitHub
Visit Enhance on GitHub.
Discord
Join our Discord server to chat about development and get help from the community.
Mastodon
Follow Enhance in the Fediverse. We're huge fans of the IndieWeb!