Notes on a Hobby Project, or a Foray into HTMX

by
Tags: , ,
Category: ,

What is HTMX?

For many years I’ve been exclusively a back-end developer, using Java and more recently using Go. I have observed from a distance the churn in front end frameworks and the rise of Javascript-based single-page apps as the de facto thick client.

I also happen to work for an excellent company that puts on an annual technical conference. A few years back, I was very intrigued by a talk given by Carson Gross. He described his invention, HTMX, a Javascript library that enables hypermedia controls in HTML pages.

The idea is that the web browser was initially intended to render hypermedia (HTML) content. The fact that RESTful servers send JSON back and forth means that front ends need to interpret that JSON payload and use it to update their local state. The way that HTML ought to work is that the browser receives HTML and renders it. End of story. The browsers is completely ignorant of the meaning of the HTML. It simply displays it to the user.

The genius of HTMX is that it embraces this hypermedia idea. In current HTML, there are two ways to tell a server that we want new content: the anchor element, and the form element. Only those elements will trigger a HTTP request, which results in an entirely new page being returned by the server and loaded into the browser.

Carson Gross asked the question: Why can’t every element have the potential to send an HTTP request? I thought this was a great question and thought that if I ever did do any front end development, I would explore the idea of using HTMX.

The Need

So, the next thing to know about me is that I am a hockey nut. My family are hockey nuts. My friends are hockey nuts. In particular, we are Philadelphia Flyers nuts. And at the start of every new season, I want to familiarize myself with the incoming players. There are websites that one can use to get the roster: https://www.nhl.com/flyers/roster, for example. However, when I am trying to learn the players, I want to be able to see someone skating, observe their number, and have something tell me who that player is based upon the number. The NHL roster lists the players alphabetically. That does not help me.

The Project

So what does a software developer do? Spends hours and hours solving such a problem. Well, I wanted to try out HTMX anyway, so this seemed like the perfect project.

Why HTMX and not a more standard single-page app approach? There are two factors that went into my choice. First, I find the idea of using the web browser as a hypermedia viewer to be appealing. Second, my timeline was short. The season was starting, and I wanted this application quickly. While I’m not an HTML expert, I thought I could refresh myself quickly enough to be able to produce HTML on the server side. This would take me orders of magnitude less time than learning one of the current single-page stacks.

Using HTMX also allows me to simply pass HTML to the browser. I wasn’t exactly sure how I was going to design this application, and I knew I’d be making design decisions as I went along. If I were to use a single-page approach, I would have to make changes in three places: the client app, the JSON payload, and the server side. By using htmx, when I wanted to change "payload", I simply delivered different HTML snippets, and the browser merely rendered them with no regard to the semantics. If it was valid HTML, it got rendered. For a simple project, this seemed like a great approach.

One potental issue however is that my application server now becomes concerned not only with the business layer but also with the presentation layer. Some might object to this as a violation of Separation of Concerns. For my project, I didn’t consider this too great of a problem. In a web app, presentation has to be handled somewhere. If it’s not handled by the client, it’s handled at the server. And if it’s handled the server, it can be recognized as a separate concern, and managed by properly organizing modules.

My app started out as very simple. One web page that would display the Flyers roster, ordered by sweater number. And it was humbling to see how unfamiliar I had gotten with basic HTML syntax. But I got that working, and it did the job.

But that didn’t require any HTMX.

So the next thing I added was the ability to choose a different team. The page started out simply as a table, but now I wrapped the table in a form with a select dropdown. Here is a bit of the HTML:

	<body>
		<form hx-trigger="change" hx-get="/roster/players-for-team" hx-target="#player-table-body" hx-swap="outerHTML">
			<label for="team-select">Team</label><select name="team" id="team-select">
				<optgroup label="Metropolitan">
					<option value="CAR">hurricanes</option>
					<option value="CBJ">blue jackets</option>
					<option value="NJD">devils</option>
					<option value="NYI">islanders</option>
					<option value="NYR">rangers</option>
					<option value="PHI" selected>flyers</option>
					<option value="PIT">penguins</option>
					<option value="WSH">capitals</option>
				</optgroup>

The hx-trigger tells the browser that I want it do take an action whenever an element of the form changes. The hx-get says that when the trigger happens, the browser should do a get to the provided path /roster/players-for-team. The hx-target says that whatever that GET request returns should be inserted into the html element with that id.

This is that element:

	<table style="width: 100%" id="player-table">
		<thead>
			<tr>
				<th style="width: 10%">Number</th>
				<th style="width: 35%">Name</th>
				<th style="width: 15%">Position</th>
				<th style="width: 10%">Height</th>
				<th style="width: 10%">Weight</th>
				<th style="width: 10%">Age</th>
			</tr>
		</thead>
		<tbody id="player-table-body">
			<tr>
				<td>5</td>
				<td>Zamula, Egor</td>
				<td>defense</td>
				<td>6'3"</td>
				<td>200</td>
				<td>24</td>
			</tr>

The thing here is that the GET call to my server does not return a full HTML page. It returns a snippet of HTML, namely table rows. These rows are swapped in to the existing page. No flicker. No page refresh. It almost looks like a single-page app.

I should back up here and explain how my app is getting the information that it’s showing. My first inclination was to fetch the NHL roster web page and scrape the HTML for the information I wanted, but then one of my way smarter friends pointed me to this website: https://github.com/Zmalski/NHL-API-Reference. So there’s an NHL RESTful API. Who knew?

So my app gets a request and then itself makes a request to the NHL API. It gets the roster and turns it into the HTML I want.

The code above shows that in my HTML form, I have a select element, which I populated with the teams in the league. Whenever a new team is selected, a GET request fetches the players for that team.

Feature Requests

After I had the bare bones of the app up and running, I put it up on Heroku and shared with some hockey friends. And the feature requests started.

One request was to provide some of the personal detail on players (height, weight, age, position).

I added a drop-down:

	<legend>Sort by</legend>
	<input type="radio" id="number" name="sort" value="number" checked>
	<label for="number">Number</label>
	<input type="radio" id="name" name="sort" value="name">
	<label for="name">Name</label>
	<input type="radio" id="position" name="sort" value="position">
	<label for="position">Position</label>
	<input type="radio" id="height" name="sort" value="height">
	<label for="height">Height</label>
	<input type="radio" id="weight" name="sort" value="weight">
	<label for="weight">Weight</label>
	<input type="radio" id="age" name="sort" value="age">
	<label for="age">Age</label>

This will add a sort parameter to GET request, which gets implemented on the server side, sorting the table rows by the requested key. And again, all the browser sees is an HTML snippet which it drops into the same location.

Server Side Rendering

The Go standard library has a templating library, which I have used in the past. But for this project I chose Gomponents. Using this library, I could write go code like this:

	func Form() g.Node {
		return h.Form(
			g.Attr("hx-trigger", "change"),
			g.Attr("hx-get", "/roster/players-for-team"),
			g.Attr("hx-target", "#player-table-body"),
			g.Attr("hx-swap", "outerHTML"),
			TeamSelect(),
			SeasonSelect(),
			SortChoice(),
		)
	}

The foundation of the gomponents library is the Node, which gets built up as shown in the above function, and then in the request handler (the code that actually responds to the GET request) the Node is constructed and then rendered:

	func createPlayersForTeamHandler(logger *slog.Logger) http.Handler {
		logger.Info("creating players for team handler")
		return http.HandlerFunc(
			func(w http.ResponseWriter, r *http.Request) {
				logger.Info("playersForTeam")
				err := r.ParseForm()
				if err != nil {
					http.Error(w, "Error", http.StatusInternalServerError)
					return
				}
				team := r.FormValue("team")
				sortBy := r.FormValue("sort")
				season := r.FormValue("season")
				logger.Info("formValues", "team", team, "sortBy", sortBy, "season", season)
				ps := nhle.NewPlayerService()
				players, err := ps.Players(team, season)
				if err != nil {
					http.Error(w, "Error", http.StatusInternalServerError)
					return
				}
	
				sort.Slice(players, makeSortFunc(players, sortBy))
	
				err = TableBody(players).Render(w)
				if err != nil {
					logger.Error("Error rendering view", "error", err)
					http.Error(w, "Error", http.StatusInternalServerError)
				}
			})
	}

The TableBody(players) function constructs the Node object, upon which we then call the Render function to write to the HTTP response writer.

Thoughts

Is HTMX going to replace all single-page apps? Certainly not. But I think it has great potential for replacing or streamlining WEB 2.0 style apps. My little project could very well have been written in WEB 2.0 style. All it does fetch a table of players for a team. HTMX simply makes that interaction smoother and more visually pleasing.

So that’s it. A fun little project where I learned HTMX, re-learned a bit of HTML, and used what I consider a great server-side rendering library. The code is here: https://github.com/StephenGriese/roster. The actual application is here: https://roster.griese.us/. I would sincerely appreciate any feedback or suggestions for improvement.