Using pure functions as views (with htmx and alpine.js)

In the past days I got my hands dirty with htmx and alpine.js

It was super refreshing and an smooth learning curve.

A lil steep and kind of depressing at the beginning, but once you get the hang of it, it was a breeze of fresh air in my development toolkit.

Here I want to get a bit into the details of how I used htmx and alpine.js to create a simple app, by focusing on the backend part.

Namely into how I used pure functions as views.


The idea is simple: instead of using a template engine (like nunjucks, jsx ssr, etc.), I just use a pure function that returns a string.

const view = (data) => {
  return `

this can be used in your routes/controllers simply by calling the function, passing in data if needed, and return it as a response.

example with htmx and alpine

A view part of a web app Iā€™m currently working on:

<div id="chats">
      .map(chat => `
          ${chat.replace('.json', '')}

In this case, Iā€™m returning parts of the html that will be used by htmx to update the DOM and by alpine to update the state and the UI.

Another example I like is the following:

<div id="messages" hx-swap="scroll:bottom">
  hx-swap="beforeend scroll:bottom"
  hx-on:htmx:after-request="this.disabled=false;setTimeout(() => this.focus(), 20)"
  x-on:keyup.enter="setTimeout(() => {message = '';pristineChat = false}, 10)"
  class="my-message" autofocus type="text" placeholder="your message">
<div style="position:fixed;bottom:3em;right:2em;" class="htmx-indicator" id="loading-message">
  <svg ...></svg>

htmx is used to send the message to the server, and update the DOM with the new messages.

it also disables the input and attaches a loading indicator to the input, so that the user knows that the message is being sent.

another cool part i find are the hx-trigger and hx-swap part.

hx-trigger is used to trigger the request when the user presses the enter key.

hx-swap in conjunction with beforeend scroll:bottom is used to append the new messages to the list of messages, and scroll to the bottom.

The POST /chat endpoint receives the user entered input and returns the LLM message in return.


Here an mjs script that I use to render the messages:

export default function messagesView(messages = []) {
  return => {
    return `
    <div hx-transition class="${message.role}-message">

This is a simple function that takes an array of messages and returns a string of html.

And on the router.mjs file:

  if (req.url === '/chat' && req.method === 'GET') {
    res.statusCode = 200
    return res.end(messagesView(messages))

Here, have a slice of pizza šŸ•