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 `
<div>
<h1>${data.title}</h1>
<p>${data.content}</p>
</div>
`
}
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">
<details>
<summary>chats</summary>
${chats
.map(chat => `
<a
x-on:click="messageDisabled=true;pristineChat=true;viewingPreviousChat=true"
hx-get="/chats/${chat}"
hx-target="#messages"
href="/chats/${chat}">
${chat.replace('.json', '')}
</a>
`).join('<br>')}
</details>
</div>
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">
${messagesView(messages)}
</div>
<input
name="message"
hx-post="/chat"
hx-trigger="keyup[keyCode==13]"
hx-target="#messages"
hx-swap="beforeend scroll:bottom"
hx-indicator="#loading-message"
hx-on:htmx:before-request="this.disabled=true"
hx-on:htmx:after-request="this.disabled=false;setTimeout(() => this.focus(), 20)"
x-bind:disabled="messageDisabled"
x-ref="message"
x-model="message"
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>
</div>
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.
views/messages.mjs
Here an mjs script that I use to render the messages:
export default function messagesView(messages = []) {
return messages.map(message => {
return `
<div hx-transition class="${message.role}-message">
${message.content}
</div>
`;
}).join('');
}
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))
}
...