Why I am looking at htmx
The reason behind me, embarking on another educational journey is simple. I applied for a job and one of its requirements was htmx. So I thought lets look at this new alien technology and to my surprise I did enjoy tinkering around with it.
My final thoughts first
- It does not have as much as learning curve + tooling around it, compare to things like ReactJS, and other frontend frameworks.
- It is a novel experience to say the least for me. I mean yes I have been using Handlebars, pug, and other templating engines but this is novel in how it changed my perspective about HTML (Just read their motivation in htmx.org).
- Testing is a bit confusing for me but I can guess we might use tools like Cypress.
- Having your backend returning HTML needs further research to be able to discuss and make rational decisions regarding how you're gonna serve other clients of your backend (mobile, IoT, other backend apps, etc).
- You have to address security concerns (XSS).
- You have to think about the way that they are using strings all over the place, no type safety, smallest typo can lead to hours of debugging, and lastly how can you make modular web apps with htmx.
- Read this article for sure.
- There are people out there who are adopting htmx. So I am positive I can find an answer to my question somewhere :).
Building a simple dummy todo web app
Bear with me, it is not really my forte to take the easy way out. But right now I wanted to just get a feeling of this tech. So you might as well try this. Then you can decide whether it worth the money to learn it or not.
We're gonna use
Steps
I split it into two part. Backend and frontend.
Backend
1. mkdir todo && cd todo
.
2. mkdir backend frontend && cd backend
.
3. pnpm init && pnpm add "express@>=5.0.0" cors --save
.
4. Add "type": "module"
to your package.json
, and this script
: "dev": "node src/index.js"
5. mkdir src && touch src/index.js
.
Write the following in it:
// @ts-check
import express from "express";
import cors from "cors";
import { buildTodosList } from "./build-todos-list.js";
import { todoRepo } from "./todos.js";
const app = express();
app.use(cors());
app.use(express.json());
// To be able to receive form data send by our form. We will create it in frontend part of this post.
app.use(express.urlencoded());
app.post("/todos", (req, res) => {
/**@type {{newTodo: string }} */
const { newTodo } = req.body;
const todos = todoRepo.create(newTodo);
res.set("content-type", "text/html");
res.status(201).send(buildTodosList(todos));
});
app.get("/todos", (req, res) => {
const todos = todoRepo.read();
res.set("content-type", "text/html");
res.status(200).send(buildTodosList(todos));
});
app.put("/todos/:id", (req, res) => {
const id = req.params.id;
const todos = todoRepo.update(id);
res.set("content-type", "text/html");
res.status(200).send(buildTodosList(todos));
});
app.delete("/todos/:id", (req, res) => {
const id = req.params.id;
const todos = todoRepo.delete(id);
res.set("content-type", "text/html");
res.status(200).send(buildTodosList(todos));
});
app.listen(
3000,
"localhost",
console.log.bind(this, "Server is up and running on port 3000")
);
6. touch src/build-todos-list.js
.
And inside it write this code:
// @ts-check
/**
*
* @param {Array<{id: string, title: string, completed: boolean}>} todos
* @returns {string}
*/
export function buildTodosList(todos) {
// Just to prevent reordering after an update. You can comment this line and use the todos directly to see the effect. In a real world app this should not happen since the sorting criteria gonna stay the same between requests unless user change it.
const sortedTodos = todos.sort((a, b) => a.title.localeCompare(b.title));
return sortedTodos.reduce((accumulator, todo) => {
accumulator += `
<li>
<input
type="checkbox"
id="todo_${todo.id}"
${todo.completed ? "checked" : ""}
hx-put="/todos/${todo.id}"
hx-trigger="click"
hx-target="#todo-list"
/>
<label for="todo_${todo.id}">${todo.title}</label>
<button
hx-delete="/todos/${todo.id}"
hx-trigger="click"
hx-target="#todo-list"
>X</button>
</li>
`;
return accumulator;
}, "");
}
7. touch src/todos.js
and then write this simple code in it:
// @ts-check
import { randomUUID } from "crypto";
export const todoRepo = {
_data: [
{
id: randomUUID(),
title: "Finish AI project",
completed: false,
},
{
id: randomUUID(),
title: "Review JavaScript notes",
completed: false,
},
{
id: randomUUID(),
title: "Buy groceries",
completed: false,
},
{
id: randomUUID(),
title: "Run for 30 min",
completed: true,
},
{
id: randomUUID(),
title: "Read AI research papers",
completed: false,
},
{
id: randomUUID(),
title: "Plan weekend trip",
completed: false,
},
{
id: randomUUID(),
title: "Exercise for 30 minutes",
completed: true,
},
{
id: randomUUID(),
title: "Write blog post",
completed: false,
},
{
id: randomUUID(),
title: "Organize desk",
completed: true,
},
{
id: randomUUID(),
title: "Schedule doctor appointment",
completed: false,
},
],
/**@param {string} newTodo */
create(newTodo) {
this._data = [
...this._data,
{
id: randomUUID(),
title: newTodo,
completed: false,
},
];
return this._data;
},
read() {
return this._data;
},
/**@param {string} id */
update(id) {
const otherTodos = this._data.filter((todo) => todo.id !== id);
const todo = this._data.filter((todo) => todo.id === id)[0];
this._data = [
...otherTodos,
{
...todo,
completed: !todo.completed,
},
];
return this._data;
},
/**@param {string} id */
delete(id) {
this._data = this._data.filter((todo) => todo.id !== id);
return this._data;
},
};
8. And then you should be able to launch your backend app with pnpm dev
command.
Frontend
First go back to the root of this project, you're terminal should be in todo/
and not todo/backend
.
1. cd /frontend && touch index.html
2. Go to the htmx.org and download it. Renamed the downloaded file to htmx.2.0.3.min.js
and placed it next to index.html
.
3. Write the following markup in your index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo</title>
<script src="./htmx.2.0.3.min.js"></script>
<link href="./index.css" rel="stylesheet" />
<base href="http://localhost:3000" />
</head>
<body>
<header class="header">
<h1>Todo app</h1>
<select id="theme">
<option value="dark" selected>Dark</option>
<option value="light">Light</option>
</select>
</header>
<main class="main">
<section class="add-todo">
<h2>Add new todo</h2>
<form class="add-todo__form" hx-post="/todos" hx-target="#todo-list">
<p>
<label for="newTodo">Todo</label>
<input type="text" name="newTodo" id="newTodo" />
</p>
<button type="submit">Add</button>
</form>
</section>
<section class="todos">
<h2>Todos</h2>
<ul hx-get="/todos" hx-trigger="load" id="todo-list"></ul>
</section>
</main>
</body>
</html>
NOTE
I am not gonna talk about CSS here. So I just let you go and read it in my repo for this post (link down below).
Now with this done you are all set. Just open this index.html
in your browser and play around with your todo app.
Important
You need to use the
base
element in your HTML document. Or<meta name="htmx-config" content='{"selfRequestsOnly": false}' />
to be able to send http requests withhx-get
and otherhx-*
attributes to external URL addresses. If you do not add it it will throw this error message at you which does not really provide much of context and help to debug it:htmx:invalidPath
kasir-barati / htmx
A simple Todo app which utilizes htmx + ExpressJS as its backend
Todo
A simple Fullstack app written in ExpressJS + htmx. It is really fun to work with htmx. I also wrote a dev.to post about it. You can read it here.
Note
But it also has its own downside. Like when your backend should serve other clients other than htmx, then what? because as you can see in my code I am returning HTML as response. But nonetheless it is a very intriguing approach to developing web applications.
How to run it
-
cd backend
. -
pnpm install
. -
pnpm dev
. - Open
frontend/index.html
in your browser.
Now you should be good to go, try to remove, add or check some of the todos ;).
Top comments (0)