I’ve been sitting down and spending some more time with the Hotwire framework after building an initial proof-of-concept and moving towards a simple 0.1 version of something. I still really like the framework, mostly because it simplifies a lot of development, without feeling like I’m sacrificing anything. Not only am I not doing as much work in the front-end, but the advantage of server-side rendering means there’s little to no state to manage in the front-end, which seems to be a huge part of front-end development. Moving from the “standard” way of writing a static webpage and populating it based on JSON data to rendering the page on the server-side is a change in how back-end development gets approached, but really it’s not as much as you may think. I stand by my comments in my original post – Hotwire is my first choice for a front-end framework.
The stack
My proof-of-concept was written in Kotlin, using Ktor as the server framework, keeping everything server-side. For this iteration, I’m still using Kotlin as my back-end server language (I want to spend some time with it, and I still think as a language it’s going to get more valuable to know over time). However, I broke all Hotwire portion out into a separate UI module, that I’m packaging with my backend via the frontend-gradle-plugin so I don’t lose that convenience of building and deploying a single jar file. I added in Mongo (as opposed to hard-coding my data like I did in the original proof-of-concept), and moved from Ktor to Spring Boot mostly because I’m more familiar with it so it seems more “natural” to me. So the current setup looks like this:
- Hotwire (Turbo and Stimulus) for front-end rendering and interaction, bundled via Webpack
- Material Bootstrap for front-end components
- Kotlin (built with Gradle) on the back-end, using Spring Boot as the web framework
- Apache Freemarker as the templating engine
- Mongo for a datastore
The concept
Basically, I’m working on implementing the idea about “doing online lessons like Sqweee sales presentations.” I’m storing how the lesson is organized in mongo, and loading the “slides” from URLs listed in the mongo document. The lesson player looks a lot like the slideshow example from the Stimulus handbook. The biggest difference is that instead of HTML content, I’m loading the lesson “slide” content itself via iframe, mostly because <turbo-frame></turbo-frame>
doesn’t seem to support sandboxing. The mongo document lists how long each “slide” displays and the Stimulus controller handles events, manages the display timer, and controls which “slide” is visible at the moment.
Because each “slide” is really just a webpage shown in an iframe, anything that can be done in a webpage can be done on each page of the lesson (with some exceptions, I’d like to this to be a kid-friendly utility, so I’m sandboxing the iframes to block scripting and try to protect privacy – the idea is that any scripting will have to be in the web page itself, and can’t “phone home” to a remote server). That means they should be less like some of the Google Slides “online activities” I saw when my son was doing online school, and (hopefully) closer to things like ABC Mouse. Asking teachers to build fully interactive lessons is thoroughly above and beyond their job descriptions, but I think we should be bringing more specialists other than teachers into the education process anyways.
The Hotwire refactor is still in progress (vs. the initial start with Angular and Kotlin), but I like the way it builds, runs, and the simplicity it’s added to the application.
Lessons learned (so far)
First and foremost, the Turbo documentation isn’t as explicit about this as it probably should be, but you can’t just make an AJAX call to your back-end, return HTML back containing a <turbo-frame></turbo-frame>
tag with the appropriate id
value set, and have new content. Instead, you actually need to update the <turbo-frame></turbo-frame>
‘s src
attribute. That will trigger a call to your back-end, which should then return the updated <turbo-frame></turbo-frame>
element, complete with new content, at which point Turbo will update your application.
If you’re using a Stimulus value
, the myVarValueChanged()
will only fire if the value itself is different from the previous value. In may case, I’m updating a currentPageValue
every time it was time to display the next page. That currentPageValueChanged()
handler checks to see if it should automatically move on after a set amount of time. If so, it updates a displayForValue
that controls the timer for automatically updating the page. This timer is set and started in the displayForValueChanged()
handler function. The problem arises when 2 consecutive pages should display for the same amount of time (e.g. page 3 and 4 both display for 10 seconds each). In Stimulus, values represent controller-wide state, so only firing the myVarValueChanged()
when the new value of myVar
is different than the old value makes absolute sense. However, if you find yourself in an edge case where consecutive values can be equal, you’ll want to manually call your value changed handler.
If you’re used to traditional web development, where your webpage calls your back-end and send back JSON data, then you’ll need to remember the whole server-side rendering approach forces you to deal with your errors on the spot, not just send them back to the client. An early version of the back-end logic (before I changed how to pick a lesson to launch) actually had something similar to the following in my code (comment included):
@PostMapping("/lesson", produces = [MediaType.TEXT_HTML_VALUE]) fun loadLesson(selectionData: LessonSelection, model: ModelMap): String { if (validationPassed(lessonData)) { val lesson: Lesson = getLesson(selectionData) model.addAttribute("lesson", lesson) return "lesson-player" } else { // TODO: RETURN A 400 ERROR } }
Here’s the thing – // TODO: RETURN A 400 ERROR
makes no sense in server-side rendered content. These endpoints return HTML. If there’s an error, rather than returning an error, I should be returning the page with an error message populated in the template. It’s a minor thing, but I found myself instinctively doing things like this because that’s how I normally do them a lot while writing back-end logic. Server-side rendering means you can’t just return an error and just let it be handled in the UI. The back-end is in charge of validation and checking for errors, and it’s in charge of dealing with them.
1 of the biggest takeaways from working in Stimulus is that you’re no longer dealing with state in the front-end, which is probably the biggest headache I’ve encountered in front-end development. Even the better state management libraries seem over-engineered at best. State management in Hotwire follows a simple premise, Javascript on the front-end handles events and manages toggles (checkboxes, etc.), but if anything needs to actually change, you need to call the back-end and deal with it there. It’s simple, clean, and allows your front-end to be stateless, you’re back-end can be stateless, and state is exclusively handled in the database.
Stimulus values and targets can make it seem more complicated than it actually is, especially because part of the power of Stimulus is that it makes things like values and targets work a lot like variables. Targets are used for easily referencing elements in your HTML without having to write document.querySelector()
calls to find them. Values are controller-level variables with the benefit of built-in callback functions you can use to automate actions on changes.
1 place where the Hotwire framework does fall short is when you want your back-end to be an API. That’s generally not something you’re actually going to need, but for people who are making a first-class front-end for their API, you’ll want a different front-end framework.
Building an application using Hotwire feels like getting back to the basics of simple HTML and vanilla Javascript. That’s because after a little bit work to set up the application, you’re really just writing…well…a lot of HTML and vanilla Javascript. Hotwire does a great job of largely staying out oi your way. The Hotwire framework has an opinionated view of how web applications should be built, and it enforces that largely by what it doesn’t implement. There’s no built-in HTML client, or native DOM rendering – because you’re not supposed to be calling a server to get JSON and using that to update your application. If you want to dynamically update your front-end, you need to use a <turbo-frame></turbo-frame>
or a <turbo-stream></turbo-stream>
. Either way, you’re server-side rendering, because Turbo expects HTML to be returned from the server. You can modify your UI via Javascript, but you’re doing it without the data-binding you see in a lot of front-end frameworks. For that kind of behavior, you’re going to have to lean on your back-end and template renderer.
While I can’t speak for the amount of effort needed to truly master the Hotwire framework, it certainly seems to have a low bar to competency. After reading over the getting started guide, I was able to build a proof-of-concept with minimal trouble. Most of the work I had taking that and turning it into a first pass at a demo application was in packaging my front-end up with Webpack, and then copying that into my Spring Boot resources
directory so I could serve everything up from 1 jar. The handful of Hotwire-specific things I’ve needed to look up I’ve easily found answers too in the handbook or reference guide on the Turbo or Stimulus sites. That kind of ease-of-use with good documentation has made it easy to build a web application without breaking things up into multiple projects that are tightly coupled, but still in different repositories. All in all, Hotwire lives up to its promise to offer “…a simpler, more productive development experience in any programming language…”