How it started
One and a half month ago, I learned the existence of this new fancy pants technology that was called HTMX. It basically tried to answer the question “What if JavaScript does not take over the world?” and giving us another attempts to be the antidote in this hyper-javascript-driven web development that has reach fever level insanity.
I personally has always been on the sideline watching the front-end web being reinvented every 6 months, as I myself was not a Web Dev. I primarily was an Android Dev but only switched to Backend in the past 2 years.
I wasn’t particularly want to learn Web Dev due to how JavaScript are 1) Mind Numbing, and 2) Contagious, as once you adopt JavaScript, everything becomes JavaScript. But I kept wanting some part of those Fullstack Pie. This is where HTMX intrigues me. I mean, side-stepping JavaScript and brings back old Web 1.0 development but “M O D E R N”? Hell yeah..?!
Now, long story short, I made myself a simple website that acts as a personal blog, this very place where you read this very story. I originally intended this to be just a one off project, but I fell in love with how it works that I kept going at it until I suddenly have usable blogging tools.
Caveat
Before we continue, I want you to put yourself in my shoes, so when I either praise or woes about all of my stacks, you can see where I’m coming from.
First, I don’t have any proper frontend web experience ever. I have dabbled with HTML, CSS, and JS here and there and tried to work with React (and React Native) to supplement my Android Dev background, but it does not go far professionally.
Secondly, I have Backend Dev experience. I’ve been personally work as a Backend in the past 2 years both professionally and as a hobby. My language of Choice is Go but I also have pretty good understanding of Python and Ruby on Rails.
Finally, My prior experiences are mostly in Android Native Development. So I am very used to dealing with Markup Language to templates UI. (Android uses XML to templates it’s UI until very recently, where we switched to declarative syntax via Jetpack Compose).
Tech Stacks
I made this website using 3 + 1 basic ingredients: HTMX, Go, TailwindCSS + Go Templating. I mostly use vanilla tools with some exceptions:
- Labstack Echo as the HTTP Server
- GORM as the ORM
Everything else is vanilla, including Templating, Tailwind, and the Go setups. I use PostgresSQL as the DB and Fly.io as the service hosting.
What I’ve built
You can just look around this very web/blog to see what’s there, or see the demo video above. This web/blog has:
- Infinite Scrolling of Posts
- Server-Side Rendered Markdown to HTML in real time. (See this for older demo)
- OAuth Login
- Search that matches several fields (This is painful to do with ORM, I would probably remove GORM after this)
- Cute Tagging system
TL;DR
I like it a lot!!. I find it incredibly easy and intuitive to do. I felt like my background in Android and Backend both takes part in the process of understanding the development process. But I also have lots of issues with it.
Now, Let’s break it down
Pros
HTMX ❤️ Go Template
HTMX goes very well with Go native templating. I can inject URLs, IDs, and even HTMX Attributes on the fly as needed. I can add declarative conditional UI state directly in the HTML like this:
1<div {{if .FormMeta.IsLastItem}}
2 hx-get="/posts?page={{.FormMeta.Page}}&pageSize=10"
3 hx-trigger="revealed"
4 hx-swap="afterend"
5 {{end}}>
Where it will only have htmx attributes whenever the Item is marked as the last Item.
Go Template ❤️ Tailwind
Golang templating is also very useful when abstracting common styles without the need to dabbles with CSS class. For example:
1{{define "style_common_width"}}
2xl:w-3/5 lg:w-4/5 sm:w-10/12 xs:w-11/12
3{{end}}
will give me instant sets of reusable styles that I can just plop whenever I need this sets of css class attributes, which I use like this:
1<div class="bg-slate-200 dark:bg-slate-900 rounded-md p-4
2 {{template "style_common_width"}}
3 self-center mx-auto px-auto my-4">
4 ...
5</div>
See that I also can just add more css class as needed, so I do not constrained with the rules of css classes. I even can compose styles to create even more elaborate component:
1{{define "style_base"}} // the base implementation
2 focus:border-transparent
3 focus:outline-none
4 focus:outline-sky-600
5 border-transparent
6{{end}}
7
8{{define "style_button_primary"}}
9 {{template "style_base"}} // reusing the base implementation
10 bg-teal-800 p-2
11 my-3
12 drop-shadow-md
13 text-slate-100
14 px-8 text-lg
15 border-none
16{{end}}
Zero Effort List
In my Android Dev days, dealing with list of items are always painful. You have the choice of RecyclerView
, incredibly powerful yet very frustrating recycling list, or very rudimentary ArrayList
that basically will chugs everytime you have more than 50 items to display.
Dealing with pagination is also very hard to do as paging3 library has the complexity so high it became a meme and is very painful to deal with.
On the contrary? this is how I implement infinite scroll of items in HTMX + Template:
1<!--in the index.html-->
2<div id="post-list">
3 {{template "post_list" .Posts}}
4</div>
5
6<!--in the post_list.html-->
7{{define "post_list"}}
8 {{range .}}
9 {{template "post_item" .}}
10 {{end}}
11{{end}}
12
13<!--in the post_item.html-->
14{{define "post_item"}}
15<div {{if .FormMeta.IsLastItem}}
16 hx-get="/posts?page={{.FormMeta.Page}}&pageSize=10"
17 hx-trigger="revealed"
18 hx-swap="afterend"
19 {{end}}>
20 <div class="...{{template "style_common_width"}}">
21 <h2 class="text-start">
22 <a href="/posts/{{.ID}}">{{.Title}}</a>
23 </h2>
24 ...
25 {{template "tag_list" .Tags}}
26 </div>
27</div>
28{{end}}
This simple snippets of HTML handles the followings behavior:
- Please renders me list of
Posts
- Each
Post
inPosts
renders:- parent div with style
{{template "style_common_width"}}
- clickable Title text whichs navigates to
/posts/:id
- List of tags from
Post.Tags
- Whenever it marked with
IsLastItem
please:- Loads more by calling
GET /posts
with specifiedpage
andpageSize
- Do it whenever the Item is
revealed
on the screen - Append the response
after
theend
of this very item.
- Loads more by calling
- parent div with style
And all of that is declaratively stated and infinitely reusable! Crazy!! 🤯🤯🤯
You do very little HTMX
Contrary on how I make big deal out of it, in actuality I use very little of HTMX. In total I only use htmx 12 times, and 6 of those are me eagering myself using htmx for something that can be done by simple anchor tag.
But wait, that’s a good thing?
Yes, actually! great tools should be something that stops once it’s usefulness ends. And HTMX is one hell of a great tools. I’m not forced to use it, but when I do, it solves real problems.
I’ve only effectively use HTMX 6 times for the whole project. Yet with only those 6 I solve major interaction issues that usually warrants a framework / some JS codes. It is incredibly cheap to just use htmx. Even just a little.
Cons
Mediocre Documentation
The HTMX Documentation looks so simple and it deceptively looks complete. But in reality, there are many unexplained context that lost (at least on me) that only solvable if you’re somewhat already familiar with HTML DOM and HTML Events.
Given this usecase:
I want to make a text input which triggers hx-post whenever I press either Cmd+Enter or Ctrl+Enter
Seems easy enough, but somehow it took me several hours digging through docs, GitHub issues, and W3Schools to understand how to use hx-trigger
with keyboard bindings. In the HTMX’s own example, they gives us this:
1hx-trigger="click, keyup[altKey&&shiftKey&&key=='D'] from:body"
Which:
- Does not explain when to use
altKey
and when to comparekey
with value of'D'
- Does not even works in Mac 🫠🫠
Turns out, hx-triggers
could be triggered by HTML Keyboard Events. Where special keys such as ctrl
, shift
, and Cmd
(metaKey
???) are boolean atrributes and common keyboard keys can be matched by any char
value that the keyboard keys represent. hence key=='D'
in the example above.
So to solve the original usecase, I can use followings hx-trigger
:
1hx-trigger="keydown[metaKey&&key=='Enter'], keydown[ctrlKey&&key=='Enter'], tag"
Where tag
is the target id
of the HTML element.
HTML logic sliperry slopes
Have access to declarative logics in the HTML is nice and all, but How far is too far? At some point I have a HTML templates that looks like this:
1<div
2 hx-get="/posts?page={{.FormMeta.Page}}&pageSize=10{{if .FormMeta.PublishedOnly}}&status=published{{end}}{{if .FormMeta.SortQuery}}&sortBy={{.FormMeta.SortQuery}}{{end}} {{if .FormMeta.Keyword}}&search={{.FormMeta.Keyword}}{{end}}"
3 hx-trigger="revealed"
4 hx-swap="afterend">
5 ...
6</div>
Which is very hard to read but also very hard to debug. As Templates does not throws error codes that is easy to digest. You have to guess a lot of it.
This can happend so easily and naturally as you originally want to build simple hx-get
to fetch some item, but then you add 1 query param, and more query params, and more and more. And suddenly, you have this unwieldly long conditional statement that is very brittle.
I have not even refactor the code above so you can see it in all of it’s glory in the GitHub repo of this very blog.
You cannot fully escape from JavaScript
As you might guess from the demo video up above, I ultimately had little bit JavaScript to help some stuff, like Image Uploading and Confirmation/Loading Dialog (I’m using SweetAlert for quick and easy solution right now).
Though to be fair, I’m using Hyperscript as opposed to JavaScript on several Dialog triggers (please spare me 🙇🏽♂️🙇🏽♂️🙇🏽♂️🙇🏽♂️).
ORM is Great, until suddenly it’s the worst!
I started this naively by thinking “surely, for a simple blogpost with entities count below 10 using ORM would be correct, no?”, and the answer is “hell no” 🤣.
Initially it’s fun and dandy when each model is unrelated and all I need is simple CRUD. But once I start to join things together, it became pretty messy. Let’s take the worst example: article search.
I want my search to look for matching content not only in the title and the content of the post, but also in the tags related to the post. The thing is, tag located in different table, and the relation between the two is many to many
(as 1 tag can be related to multiple posts and a post can have multiple tags), so do that I effectively need to join them together.
I try to do it the “ORM” way where I’m suppose to not write any sql myself. But GORM does not support JOIN
followed by Conditional Statement
(Evaluate the JOIN
result). It only support eager loading with conditions (Evaluate the value before JOIN
), so I cannot just create a FULL JOIN
of both.
I then give up tried to do it the ORM way, and just use ORM but with a bit of SQL. Then I came up with this:
1db.Table("posts").
2 Select("distinct posts.id",
3 "posts.title", "posts.created_at",
4 "posts.status", "posts.updated_at",
5 "posts.published_at").
6 Joins("full join post_tags on posts.id = post_tags.post_id").
7 Joins("left join tags on post_tags.tag_id = tags.id").
8 Where("lower(posts.title) like ?", wrappedKeyword).
9 Or("lower(posts.content) like ?", wrappedKeyword).
10 Or("lower(tags.title) like ?", wrappedKeyword),
11 Where("posts.status = ?", status)
Which is suspiciously just look like raw SQL but with syntactic sugar. And the best part? it does not do what I want.
So, please watch the last part where I add Where()
condition that should filters out status. All Where()
function is flattened, meaning all live in the same plane. The query above is equal to WHERE...OR...AND...
instead of my goal which is WHERE(...OR...)AND...
It took me quite a while flipping around GORM documentation. But it turns out, you could just nest the “Where” statement to simulate WHERE(..OR..)AND ...
. So I modified it into this:
1wrappedKeyword := fmt.Sprintf("%%%s%%", strings.ToLower(keyword))
2dbs := db.Table("posts").
3 Select("distinct posts.id",
4 "posts.title", "posts.created_at",
5 "posts.status", "posts.updated_at",
6 "posts.published_at").
7 Joins("full join post_tags on posts.id = post_tags.post_id").
8 Joins("left join tags on post_tags.tag_id = tags.id")
9
10dbs = dbs.
11 Where(
12 dbs.Where("lower(posts.title) like ?", wrappedKeyword).
13 Or("lower(posts.content) like ?", wrappedKeyword).
14 Or("lower(tags.title) like ?", wrappedKeyword),
15 )
16
17if status != values.None {
18 dbs = dbs.Where("posts.status = ?", status)
19}
And finally it worked! But at this point, I might as well use SQLC and write my own SQL doesn’t it?
Conclusion
I really love what HTMX provides for my use case. It strikes very nice balance for someone that has background in Android and Backend where a lot of the knowledge in each of those domains helped me creates an interpretation of how to work with front end development a la hypermedia.
It does however, still has a lot of kinks to be ironed out, and need a strict and active discipline from the developer themselves to not fall into the traps of declarative hell that is HTML templating.
There is no handholding really, as a lot of the best practices are not defined and documented and people will need to draw the line themselves where and how to do things.
That’s it! Please give me feedback in the form of email or reply to any repost I made. I’m sorry I haven’t made proper comment system yet (and also sitemaps, RSS feeds, newsletters… urgh…).