Skip to content

Commit 3cf2d76

Browse files
author
Marcus Hammarberg
committed
Adds links to code
1 parent 33ef751 commit 3cf2d76

File tree

4 files changed

+247
-4
lines changed

4 files changed

+247
-4
lines changed

_posts/2025-01-14-htmx-todo-tutorial.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ I'm going to try to avoid to make this and Old Man Rant (tm), but first let's go
2020

2121
This first post will be a bit theoretical but then we will write a Todo application (had to be one of those, right?) that stores data in Firebase and allows you to log in with Google.
2222

23+
[At the end of each post I'll give you a link to the code in this repository](https://github.com/marcusoftnet/htmx-todo-tutorial)
24+
2325
<!-- excerpt-end -->
2426

2527
## Multi-page applications
@@ -462,4 +464,4 @@ Ok - we have an 2000-ies web page up and running, using a multi-page approach an
462464

463465
In the next post I'll move this into the 2010-ies and log in using a third party provider, before creating the actual application in the last post (taking us into the 2020-ies using a non-SPA approach).
464466

465-
I'll update these post with a link to the source code, once I've written it.
467+
[Here's the code at this point](https://github.com/marcusoftnet/htmx-todo-tutorial/tree/4fe785a13f151ee08bbe5083f469d54e3862640f)

_posts/2025-01-15-htmx-todo-tutorial-II.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,4 +414,4 @@ In this post we have set up authentication against Google, created a session to
414414
415415
Not too shabby, BUT we haven't really done much with HTMx yet, let's go fully HTMx in the next post. Oh, and add data in the Firestore database, but that will be tiny.
416416
417-
The code at this point can be found here. (I'll update this further on).
417+
[The code at this point can be found here.](https://github.com/marcusoftnet/htmx-todo-tutorial/tree/7a6004505905efbe85120f2c60f215fc2f2e57c2)

_posts/2025-01-15-htmx-todo-tutorial-III.md renamed to _posts/2025-01-16-htmx-todo-tutorial-III.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
layout: post
33
title: "HTMx - tutorial part III - Building the application"
44
author: "Marcus Hammarberg"
5-
date: 2025-01-17 04:00:00
5+
date: 2025-01-16 04:00:00
66
tags:
77
- Programming
88
- HTMx
@@ -508,6 +508,6 @@ And the `edit.ejs` file looks different enough from the `new.ejs` to warren a se
508508

509509
That's it for this post. We have built a fully-fledge (albeit missing some error handling and validation, I'm happy to admit) todo application, storing the information in a collection per logged in user.
510510

511-
The code will be update there.
511+
[The code will be update here](https://github.com/marcusoftnet/htmx-todo-tutorial/tree/8c5fa67aeaa48c866cc2e2aace32d12e513f7b17).
512512

513513
I have one more thing I wanted to build and that is the counters at the end. It will require some HTMx trickery and I'll explore some options on how to achieve that, and make some very lofty promises. But that is in the next post.
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
---
2+
layout: post
3+
title: "HTMx - tutorial part IV - Updating other parts of the application"
4+
author: "Marcus Hammarberg"
5+
date: 2025-01-18 04:00:00
6+
tags:
7+
- Programming
8+
- HTMx
9+
- Node
10+
- Express
11+
---
12+
13+
This is the fourth post in a series that I'm making about HTMx that I think is utterly amazing and will change how I (we?) write web apps in the future. You are more than welcome to read from here, but it will be hard following along code-wise if you haven't stepped through [part I](https://www.marcusoft.net/2025/01/htmx-todo-tutorial.html), [part II](https://www.marcusoft.net/2025/01/htmx-todo-tutorial-II.html) and [part III](https://www.marcusoft.net/2025/01/htmx-todo-tutorial-III.html) first.
14+
15+
In the last part we built out most the actual application, which means that we are left with the fun part here. There's one section of the application that is not working yet - the footer. In the footer I wanted to show a tally of the number of items, the number of completed items, and also the number of late items.
16+
17+
Keeping track of numbers like that can be a bit messy since we need to update them when we are doing other actions, like adding, deleting or simply when time has passed.
18+
19+
HTMx has a few different strategies to handle this and I wanted to show you two.
20+
21+
<!-- excerpt-end -->
22+
23+
## Swap out of bands
24+
25+
The first strategy has a name that doesn't really roll of the tongue that easy; [Out of bands swap](https://htmx.org/attributes/hx-swap-oob/). It simply means that we can tell HTMx to do more than one update in the page based of the response. I think about it as us throwing in additional update instruction in another response.
26+
27+
It's easiest to show you with an example; let's update the counters as part as showing the list.
28+
29+
### Get the counters
30+
31+
First - let's write a function that makes the counting. I flexed my functional brain and created the following function that will do just that, but using `.map` and `.reduce` (and also looping through the list only once):
32+
33+
```javascript
34+
const getCounters = (todos) => {
35+
const today = new Date();
36+
37+
return todos
38+
.map(todo => ({
39+
late: new Date(todo.duedate) < today ? 1 : 0,
40+
completed: todo.completed ? 1 : 0
41+
}))
42+
.reduce((acc, curr) => ({
43+
total: acc.total + 1,
44+
late: acc.late + curr.late,
45+
completed: acc.completed + curr.completed
46+
}), { total: 0, late: 0, completed: 0 });
47+
};
48+
```
49+
50+
The `.map` step creates a new object for each todo and tracks if the item is late and completed. Then in the `.reduce` step we accumulate those numbers, adding a total, to get a final counter object.
51+
52+
Put that function in the `routes/todo.js` file.
53+
54+
### Updating the `GET /` end point
55+
56+
In that same file, we can now update `router.get("/")` to use this function and pass the result on to the the `views/todo/list.ejs` template:
57+
58+
```javascript
59+
router.get("/", async (req, res) => {
60+
const todos = await getAllTodos();
61+
const counters = getCounters(todos);
62+
res.render("todo/list.ejs", { todos, counters });
63+
});
64+
```
65+
66+
### Updating the `views/todo/list.ejs` template
67+
68+
Now, here is the HTMx part. In the `list.ejs` view we are going to render two snippets of HTML; the list and the counters. I KNOW - it's super weird.
69+
70+
```html
71+
<% todos.forEach(todo => { %>
72+
<%- include("todo-list-item.ejs", {todo}) -%>
73+
<% }) %>
74+
75+
<p hx-swap-oob="innerHtml:#counters">
76+
Total items: <span id="total-items"><%= counters.total %></span>
77+
| Completed: <span id="completed-items"><%= counters.completed %></span>
78+
| Late: <span id="late-items"><%= counters.late %></span>
79+
</p>
80+
```
81+
82+
The trick lies in the `hx-swap-oob="innerHtml:#counters"` attribute, which tells HTMx to swap the inner HTML of the #counters element with this element. You can [read more about the capabilities of `hx-swap-oob` here](https://htmx.org/attributes/hx-swap-oob/).
83+
84+
It basically means that the `<p hx-swap-oob>` will not be displayed in the `list.ejs`, but rather HTMX will, when the `HTTP GET` response for `/` returns the to client, swap the `innerHTML` for the `#counters` selector.
85+
86+
Let's make that work by updating the `main.ejs` footer section to this:
87+
88+
```html
89+
<footer id="counters">
90+
<p>There will be counters here when you log in</p>
91+
</footer>
92+
```
93+
94+
And, just because we can, let's clean up the `list.ejs` to include a `counters.ejs` file. Here's the updated `list.ejs` template:
95+
96+
```html
97+
<% todos.forEach(todo => { %>
98+
<%- include("todo-list-item.ejs", {todo}) -%>
99+
<% }) %>
100+
101+
<%- include("counters.ejs", {counters}) -%>
102+
```
103+
104+
And then here's the `views/todo/counters.ejs`:
105+
106+
```html
107+
<p hx-swap-oob="innerHtml:#counters" >
108+
Total items: <span id="total-items"><%= counters.total %></span>
109+
| Completed: <span id="completed-items"><%= counters.completed %></span>
110+
| Late: <span id="late-items"><%= counters.late %></span>
111+
</p>
112+
```
113+
114+
That will soon become useful...
115+
116+
## Expand the target
117+
118+
HTMx have actually written about the different strategies to handle these types of updates, but hidden in a post about tables. It's a [great read](https://htmx.org/examples/update-other-content/) and I just want to mention one strategy that we will not use: [Expand the target](https://htmx.org/examples/update-other-content/#expand).
119+
120+
That simply means, in our case, that we would include the `<footer>` in the all responses where it's needed to be displayed. That is by far the easiest to do, but also a bit crude and might not support all use-cases.
121+
122+
For example ours. And to be honest, even using the `hx-swap-oob` is a bit too clumsy for us.
123+
124+
When you think about it means that we need to include the counting of items and the `counters.ejs` template, in just about all responses our little app send back. Not that it's heavy or request intensive but it gets pretty hard to understand when reading the codebase.
125+
126+
If only there was a way to inform the counters about that they need to be updated. Some kind of ... event that we could trigger
127+
128+
## Trigger events to update
129+
130+
And sure there is... [`hx-trigger`](https://htmx.org/attributes/hx-trigger/) can respond to custom events as well as standard such as `click` or `change`.
131+
132+
And we can trigger these events, server-side, by using the `HX-Trigger` header.
133+
134+
That will create a nice event-driven system where events that happens server-side can be propagated back to the client. The client can then use the standard `hx-trigger` setting to indicate that when it should be updated.
135+
136+
I'm going to leave the `hx-swap-oob` for the `/` route, and then use the `HX-Trigger` approach for all other routes (that updates any data).
137+
138+
### Set a header using Express
139+
140+
Let's take the `router.delete("/:id")` as an example. Add the `HX-Trigger` header in the response by making the route look like this:
141+
142+
```javascript
143+
router.delete("/:id", async (req, res) => {
144+
await deleteTodo(req.params.id);
145+
res
146+
.status(204) // no content
147+
.header("HX-Trigger", '{"ITEM_UPDATED": `The ${id} item was DELETED`}')
148+
.send();
149+
});
150+
```
151+
152+
The `HX-Trigger` header can consist of whatever you want, but should at least be a name of the event, and in my case I'm passing along a little message too. I'm just that nice!
153+
154+
We can now update the `<footer>` in `main.ejs` to listen for the `ITEM_UPDATED` event like this:
155+
156+
```html
157+
<footer id="counters" hx-trigger="ITEM_UPDATED from:body" hx-get="/todo/counters">
158+
<p>There will be counters here when you log in</p>
159+
</footer>
160+
```
161+
162+
When the `ITEM_UPDATED` is triggered, which it will be using the `HX-Trigger` header, we promptly `HTTP GET /todo/counters` that will return the counters.
163+
164+
That `from:body` part is needed since the event is bubbling up from the body. Straight [from the docs](https://htmx.org/attributes/hx-trigger/)
165+
166+
> This is because the header will likely trigger the event in a different DOM hierarchy than the element that you wish to be triggered
167+
168+
### `/todo/counters` endpoint
169+
170+
Writing the endpoint is pretty straight-forward, and we can reuse the `getAllTodos` function and the `counters.ejs` template:
171+
172+
```javascript
173+
router.get("/counters", async (req, res) => {
174+
const todos = await getAllTodos();
175+
const counters = getCounters(todos);
176+
res.render("todo/counters.ejs", { counters });
177+
});
178+
```
179+
180+
There's only one problem, it's not showing up. That's because our `counter.ejs` template included that `hx-swap-oob` statement. Now, in normal cases I could just have removed that, but since I wanted this to show off both variants I'm going to add a little switch for that:
181+
182+
```html
183+
<% if(typeof addSwapOOB !== 'undefined') { %>
184+
<p hx-swap-oob="innerHtml:#counters">
185+
<%} else { %>
186+
<p>
187+
<% } %>
188+
Total items: <span id="total-items"><%= counters.total %></span>
189+
| Completed: <span id="completed-items"><%= counters.completed %></span>
190+
| Late: <span id="late-items"><%= counters.late %></span>
191+
</p>
192+
```
193+
194+
And then, only for the `/` route I can pass `addSwapOOB:true`:
195+
196+
```javascript
197+
```
198+
199+
I'm not too happy but it will help to show the different approaches.
200+
201+
### Updating the other action endpoints
202+
203+
I then went through the other endpoints that are not `HTTP GET` and added the `HX-Trigger`-header. All of these end points changes the values I want to display and should trigger an update of the counters.
204+
205+
After some refactoring they look like this example:
206+
207+
```javascript
208+
const TRIGGER_HEADER = "HX-Trigger";
209+
210+
router.put("/:id/toggle", async (req, res) => {
211+
await toggleTodoCompleted(req.params.id);
212+
const todo = await getTodo(req.params.id);
213+
214+
res.setHeader(TRIGGER_HEADER, `{"ITEM_UPDATED": "The ${req.params.id} completion was toggled"}`)
215+
res.render("todo/todo-list-item.ejs", { todo });
216+
});
217+
```
218+
219+
And now the counters are updated through the events that gets triggered
220+
221+
## Summary
222+
223+
There's immense power in the event driven approach to update parts of the UI. Not only can very complex and previously tricky relationships between different parts of the UI be created. They can also be handled in a declarative and easy-to-understand way.
224+
225+
For example, imagine that we instead of `ITEM_UPDATED` had specialized events for every typ of update; `ITEM_DELETED`, `ITEM_ADDED` etc.
226+
227+
Then different parts of the UI could subscribe on one or more of these events:
228+
229+
```html
230+
<footer id="counters" hx-trigger="ITEM_UPDATED, ITEM_ADDED from:body" hx-get="/todo/counters">
231+
<p>There will be counters here when you log in</p>
232+
</footer>
233+
```
234+
235+
Some very interesting and advanced UIs could be built this way.
236+
237+
I learned a lot by writing this series and I hope you found it useful too.
238+
239+
I think HTMx is a breath of fresh air for web developers that, like me, have got lost in SPA frameworks and JSON-to-HTML parsing. It brings the pure ideas of the web back to the forefront while still allows me to write websites that only rerenders the part of the application that has changed.
240+
241+
[The code is found here in the state that I left it in at the end of this post.](https://github.com/marcusoftnet/htmx-todo-tutorial/tree/406bda133d83410d85c52286f66a4f0124b19e6e)

0 commit comments

Comments
 (0)