turbo: Hotwired (Turbo and Stimulus). Turbo is a bit like HTMX. It is widely used and supported well in Ruby on Rails. Stimulus is a lightweight library that can be used to implement tiny bits of logic that prefer to live on the client.
nodejs: like the
turbosample but using Node.js to build and bundle the scripts, instead of Webjars. If you get serious about React, you will probably end up doing this, or something like it. The aim here is to use Maven to drive the build, at least optionally, so that the normal Spring Boot application development process works. Gradle would work the same.
react: is the
All the samples can be built and run with standard Spring Boot processes (e.g. see this getting started guide). The Maven wrapper is in the parent directory so from each sample on the command line you can
../mvnw spring-boot:run to run the apps or
../mvnw package to get an executable JAR. E.g.
Narrowing the Choices
import also have an equivalent bundle you can
require, so you can always stick to that if you prefer.
There are also choices that can be made on the server side. We have used Spring Webflux but Spring MVC would work identically. We have used Maven as a build tool, but using Gradle it would be easy to achieve the same goals. All the samples actually have a static home page (not even rendered as a template), but they all have some dynamic content, and we have chosen JMustache for that. Thymeleaf (and other templating engines) would work just as well. In fact Thymeleaf has built-in support for fragments and that can be quite useful when you are updating parts of a page dynamically, which is one of our goals. You could do that same with Mustache (probably) with a bit of work, but we didn’t need it in these samples.
Create a New Application
To get started with Spring Boot and client-side development, let’s start at the beginning, with an empty app from Spring Initializr. You can go to the website and download a project with web dependencies (select Webflux or WebMVC) and open it up in your IDE. Or to generate a project from the command line you can use
curl, starting form an empty directory:
We can add a really basic static home page at
To start building client-side features, let’s add some CSS out of the box from Bootstrap. We could use a CDN, like this for example in
That’s really convenient, if you want to get started quickly. For some apps it might be all you need. Here we take a different approach that makes our app more self-contained, and aligns well with the Java tooling we are used to - that is to use a Webjar and package the Bootstrap libraries in our JAR file. To do that we need to add a couple of dependencies to the
and then in
index.html instead of the CDN we use a resource path inside the application:
If you rebuild and/or re-run the application you will see nice vanilla Bootstrap styles instead of the boring default browser versions. Spring Boot uses the
webjars-locator-core to locate the version and exact location of the resource in the classpath, and the browser sucks that stylesheet into the page.
index.html like this:
It doesn’t do anything visible yet, but you can verify that it is loaded by the browser using the devtools view (F12 in Chrome or Firefox).
We said in the introduction that we would use ESM modules where available, and Bootstrap has one, so let’s get that working. Replace the
<script> tag in
index.html with this:
There are two parts to this: an “importmap” and a “module”. The import map is a feature of the browser allowing you to refer to ESM modules by name, mapping the name to a resource. If you run the app now and load it in the browser there should be an error in the console because the ESM bundle of Bootstrap has a dependency on PopperJS:
PopperJS is not a mandatory transitive dependency of the Bootstrap Webjar, so we have to include it in our
(Webjars use the “__” infix instead of a “@” prefix for namespaced NPM module names.) Then it can be added to the import map:
and this will fix the console error.
Normalizing Resource Paths
The resource paths inside a Webjar (e.g.
/bootstrap/dist/js/bootstrap.esm.min.js) are not standardized - there is no naming convention that allows you to guess the location of the ESM module inside a Webjar, or an NPM module which amounts to the same thing. But there are some conventions in NPM modules that make it possible to automate: most modules have a
package.json with a “module” field. E.g. from Bootstrap you can find the version and the module resource path:
CDNs like unpkg.com make use of this information, so you can use them when you know only the ESM module name. E.g. this should work:
It would be nice to be able to do the same with
/webjars resource paths. That’s what the
NpmVersionResolver in all the samples does. You don’t need it if you don’t use Webjars and you can use a CDN, and you don’t need it if you don’t mind manually opening up all the
package.json files and looking for the module path. But it’s nice to not have to think about that. There’s a feature request asking for this to be included in Spring Boot. Another feature of the
NpmVersionResolver is that it knows about the Webjars metadata, so it can resolve the version of each Webjar from the classpath, and we don’t need that
webjars-locator-core dependency (there’s an open issue in Spring Framework to add this feature).
So in the sample the import map is like this:
All you need to know is the NPM module name, and the resolver figures out how to find a resource that resolves to the ESM bundle. It uses a Webjar if there is one, and otherwise redirects to a CDN.
Note Most modern browsers support modules and module maps. Those that don’t can be used in our app at the cost of adding a shim library. It is already included in the samples.
We might as well use the Bootstrap styles now we have it all working. So how about some tabs with content and a button or two to press? Sounds good. First the
<header/> with the tab links in
The second (default inactive) tab is called “stream” because part of the samples will be exploring the use of Server Sent Event streams. The tab contents look like this in the
Note how one of the tabs is “active” and both have ids that match up with the
<nav/> - you can look at the Petclinic to see how). In a browser it looks like this:
and of course if you click on the “Stream” tab it reveals some different content.
Dynamic Content with HTMX
and then import it in
Then we can change the greeting from “Hello World” to something that comes from user input. Let’s add an input field and a button to the main tab:
The input field is unadorned, and the button has some
hx-* attributes that are grabbed by the HTMX library and used to enhance the page. These ones say “when user clicks on this button, send a POST to
/greet, including the ’name’ in the request, and render the result by replacing the content of the ‘greeting’”. If the user enters “Foo” in the input field, the POST has a form-encoded body of
value=Foo because “value” is the name of the field identified by
Then all we need is a
/greet resource in the backend:
Spring will bind the “value” parameter in the incoming request to the
Greeting and we convert it to text which is then injected in the
<div id="greeting"/> on the page. You can use HTMX to inject plain text like this, or whole fragments of HTML. Or you can append (or prepend) to a list of existing elements, like rows in a table, or items in a list.
Here’s another thing you can do:
This does a GET to
/user when the page loads and swaps the content of the element. The sample app has this endpoint and it returns “Fred” so you see it rendered like this:
There are many other neat things you can do with HTMX, and one of those is to render a Server Sent Event (SSE) stream. First we’ll add an endpoint to the backend app:
So we have a stream of messages rendered by Spring by virtue of the
produces attribute on the endpoint mapping:
HTMX can inject those messages into our page. Here’s how in
index.html added to the “stream” tab:
We connect to the
/stream using the
connect:/stream attribute and then pull event data out using
swap:message. Actually “message” is the default event type, but SSE payloads can also specify other types by including a line starting with
event:, and so you could have a stream that multiplexes many different event types and have them each affect the HTML in different ways.
The endpoint in our backend above is very simple: it just sends back plain strings, but it could do more. E.g. it could send back fragments of HTML and they would be injected into the page. The sample applications do it with a custom Spring Webflux component named
CompositeViewRenderer (requested as a feature here for the Framework), where
@Contoller method can return a
Flux<Rendering> (in MVC it would be
Flux<ModelAndView>). It enables an endpoint to stream dynamic views:
This is paired with a view named “time” and the normal Spring machinery renders the model:
The HTML comes from a template:
which in turn works automatically because we included JMustache on the classpath in
Replacing and Enhancing HTML Dynamically
HTMX can still do more. Instead of an SSE stream, an endpoint can return a regular HTTP response, but compose it as a set of elements to swap on the page. HTMX calls this an “out of band” swap because it involves enhancing content of elements on the page that are not the same as the one that triggered the download.
To see this work we can add another tab with some HTMX-enabled content:
Don’t forget to add a nav link so the user can see this tab:
The new tab has a button that fetches dynamic content from
/test and it also sets up 2 empty divs “hello” and “world” to receive the content. The
hx-swap="none" is important - it tells HTMX not to replace the content of the element that triggered the GET.
If we have an endpoint that returns this:
then the page renders like this (after the “Fetch” button is pressed):
A simple implementation of this endpoint would be
or (using the custom view renderer):
with a template “test.mustache”:
Boosting Links and Forms
Dynamic Content with Hotwired
Hotwired is a little bit similar to HTMX, so let’s replace the libraries an get the app working. Take out HTMX and add Hotwired (Turbo) to the application. In
Then we can import it into our page by adding an import map:
and a script to import the library:
Replacing and Enhancing HTML Dynamically
This lets us do the dynamic content stuff that we already did with HTMX with a few changes to the HTML. Here’s the “test” tab in
Turbo works a little differently than HTMX. The
<turbo-frame/> tells Turbo that everything inside is enhanced (a bit like an HTMX boost). And to replace the “hello” and “world” elements on a button click, we need the button to send a POST through a form, not just a plain GET (Turbo is more opinionated about this than HTMX). The
/test endpoint then sends back some
<turbo-stream/> fragments containing templates with the content we want to replace:
To make Turbo take notice of the incoming
<turbo-stream/> we need the
/test endpoint to return a custom
Content-Type: text/vnd.turbo-stream.html so the implementation looks like this:
To serve the custom content type we need a custom view resolver:
The above is a copy of the
@Bean defined automatically by Spring Boot but with an additional supported media type. There is an open feature request to allow this to be done via
The result of clicking the “Fetch” button should be to render “Hello” and “World” as before.
Server Sent Events
Turbo also has built in support for SSE rendering, but this time the event data has to have
<turbo-stream/> elements in it. For example:
Then the “stream” tab just needs an empty
<div id="load"></div> and Turbo will do what it was asked (replace the element identified by “load”):
Both Turbo and HTMX allow you to target elements for dynamic content by id or by CSS style matcher, both for regular HTTP responses and SSE streams.
and with an import map in
Then we are in good shape to replace the piece of the main “message” tab that we did with HTMX before. Here’s the tab content covering just the button and custom message:
data-* attributes. There is a
controller (“hello”) declared on the container
<div> that we need to implement. Its action in the button element says “when this button is clicked, call the function ‘greet’ on the ‘hello’ controller”. And there are some decorations that identify which elements have input and output for the controller (the
Controller is registered with the
data-controller name from the HTML, and it has a
targets field that enumerates all the ids of elements that it wants to target. It can then refer to them by a naming convention, e.g. “output” shows up in the controller as a reference to a DOM element called
You can do more or less anything you like in the
Controller, so for example you could pull some content from the backend. The
turbo sample does that by pulling a string from the
/user endpoint and inserting it in an “auth” target element:
Add Some Charts
index.html (remember to add the
<nav/> link as well):
It has an empty
<canvas/> that we can fill in with a bar chart using Chart.js. In preparation for that we declared a controller called “chart” in the HTML above and labelled the target element for it with
data-*-target. So let’s start by adding Chart.js to the application. In
and the new controller implementing the “bar” and “clear” actions from the buttons in the HTML:
To service this we need a
/pops endpoint with some chart data (estimated world population by continent according to Wikipedia):
The sample app has a few more charts, all showing the same data in different formats. They are all serviced by the same endpoint illustrated above:
Code Block Hiding
and then we can add a new tab with some “code snippets” (just junk content in this case):
It looks like this if the user selects the “One” block type:
The thing that drives the behaviour is the structure of the HTML, with one element labelled “primary” and alternatives as “secondary”, then a nested
Dynamic Content With Vue
and add it to the import map in
index.html (using a manual resource path because the “module” in the NPM bundle points to something that doesn’t work in a browser):
Then we can write a component and “mount” it in a named element (it’s an example from the Vue user guide):
To receive the dynamic content we need an element that matches
So the templating happens on the client, and it is triggered by a click using
v-on from Vue.
If we want to replace Hotwired with Vue we could start with the content on the main “message” tab. So we can replace the Stimulus controller bindings with this, for example:
and then hook the
greeting properties in through Vue:
created hook is run as part of the Vue component lifecycle, so it’s not necessarily going to be run precisely the same time as Stimulus did it, but it’s close enough.
We can also replace the chart picker with a Vue, and then we can get rid of Stimulus, just to see what it looks like. Here’s the chart tab (basically the same as before but without the controller decorations):
The sample code also has “pie” and “doughnut” in addition to the “bar” chart type, and they work the same way.
Server Side Fragments
Vue can replace the entire inner HTML of an element using the
v-html attribute, so we can start to re-implement the Turbo content with that. Here’s the new “test” tab:
It has a click handler referring to a “hello” method, and a div that is waiting to receive content. We can attach the button to the “hi” container like this:
To make it work we just need to remove the
<turbo-frame/> elements from the server side template (reverting to what we had in the HTMX sample).
Dynamic Content with React
We can get started and try it out without changing too much. The sample code will end up looking like the
react-webjars sample if you want to peek. First the dependencies in
and the module map in
With those in place you can import the functions and objects they define:
Because they are not really ESM modules you can do this at the “global” level in a
<script/> in the HTML
<head/>, e.g. where we import
bootstrap. Then you can define some content by creating a
React.Component. Here’s a really basic static example:
render() method returns a function that creates a new DOM element (an
<h1/> with content “Hello, world!”). It is attached by
ReactDOM to an element with
id="root", so we’d better add one of those as well, for example in the “test” tab:
If you run that it should work and it should say “Hello World” in that tab.
The component defines a custom element
<Hello/> that match the class name of the component, and conventionally starts with a capital letter. The
<Hello/> fragment is an XJS template, and the component also has a
render() function that returns an XJS template. Braces are used for interpolation, and
props is a map including all the attributes of the custom element (so “name” in this case). Finally there is that
The React user guide advises against using
@babel/standalone in a large application because it has to do a lot of work in the browser, and the same work can be done once at build time which is more efficient. But it’s good for trying stuff out, and for apps with small amounts of React code, like this one.
Basic Event and User Input Handling
We are now in a position where we can migrate the main “message” tab to React. So let’s modify the
Hello component and attach it to a different element. The message tab can be stripped down to an empty element ready to accept the React content:
We can anticipate that we will need a second component to render the authenticated user name, so let’s start with this to attach some code to the element in the tab above:
Then we can define the
Auth component like this:
The lifecycle callback in this case is
componentDidMount which is called by React when the component is activated, so that’s where we put our initialization code.
The other component is the one that transfers the “name” input to a greeting:
render() method has to return a single element, so we have to wrap the content in a
bind() on all the component methods that we want to use as listeners (
change in this case).
To migrate the rest of the Stimulus content to React we need to write a new chart chooser. So we can start with an empty “chart” tab:
and attach a
ReactDOM element to the “chooser”:
ChartChooser is a list of buttons encapsulated in a component:
We also need the chart module setup from the Vue sample (it won’t work in a
Chart.js isn’t shipped in a form you can import into a Babel script. We import it in a separate module, and
Chart has to be defined as a global so we can still use it in our React component.
Server Side Fragments
To render the “test” tab with React we can start with the tab itself, empty again to accept content from React:
with a binding to the “root” element in React:
Then we can implement the
<Content/> as a component that fetches HTML from the
dangerouslySetInnerHTML attribute is delibrately named by React to discourage people from using it with content that is collected directly from users (XSS issues). But we get that content from the server so we can put our trust in the XSS protection there and ignore the warning.
If we use that
<Content/> component and the SSE loader from the sample above then we can get rid of Hotwired altogether from this sample.
Building and Bundling with Node.js
The first thing you will need is Node.js. There are many ways of obtaining it, and you can use whatever tools you want. We will show how to do it with the Frontend Plugin.
Let’s add the plugin to the
turbo sample. (The final result is the
nodejs sample if you want to peek) in
Here we have 3 executions:
install-node-and-npm installs Node.js and NPM locally,
npm install and
package.json to run them all. If you have
npm installed you could
npm init to generate a new one, or just create it manually:
Then we can build
You will see the result is a new directory:
It is useful to have an quick way to run
npm from the command line, when it is installed locally like this. So once you have Node.js you can make it easy by creating a script locally:
Make it executable and try it out:
Adding NPM Packages
Now we are ready to build something, let’s set up
package.json with all the dependencies that we had in Webjars until now:
./npm install (or
./mvnw generate-resources) will download those dependencies into
It’s OK to add all the downloaded and generated code to your
Building with Rollup
The Bootstrap maintainers use Rollup to bundle their code, so that seems like a decent choice. One thing it does really well is “tree shaking” to reduce the amount of Javscript you need to ship with your application. Feel free to experiment with other tools. To get started with Rollup we will need some development dependencies in
package.json and a new build script:
/index.js at runtime. This is
src/main/js/index.js we would have just one
index.html, for instance at the end of the
We will keep the CSS for now, and we can deal with a local build for that later. So in
index.js we have all the
<script> tag contents mushed together (or we could have split it up into modules and imported them):
If we build and run the app it should all work, and Rollup creates a new
target/classes/static where it will be picked up by the executable JAR. Because of the action of the “resolve” plugin in Rollup, the new
index.js has all of the code that is needed to run our application. If any dependencies are packaged as a proper ESM bundle, Rollup will be able to shake the unused parts of them out. This works for Hotwired Stimulus at least, and most of the others get included wholesale, but the result is still only 750K (most of it Bootstrap):
Building CSS with Sass
So far we have used plain CSS bundled in some NPM libraries. Most applications need their own stylesheets and developers prefer to work with some form of templating library and build time tooling to compile to CSS. The most prevalent such tool (but not the only one) is Sass. Bootstrap uses it, and indeed packages its source files in the NPM bundle, so you can extend and adapt the Bootstrap styles to your own requirements.
We can see how that works by building the CSS for our application, even if we don’t do much customization. Start with some tooling dependencies in NPM:
which leads to some new entries in
This means we can update our
rollup.config.js to use the new tools:
The CSS processors look in the same place as the main input file, so we can just create a
src/main/js and import the Bootstrap code:
Customizations in SCSS would follow that if we were doing it for real. Then in
index.js we add imports for this and the Spring utils library:
and re-build. This will lead to a new
That’s it. We have one
Bundling a React App with Node.js
To finish up we can apply the same ideas to the
@babel/standalone. We can start from the
react-webjars sample and add the Frontend Plugin as above (or otherwise acquire Node.js), and create a
package.json either manually or via the
npm CLI. We will need the React dependencies, and also the build time tooling for Babel. Here’s what we end up with:
We need the
commonjs plugin because React is not packaged as an ESM and the imports will not work without doing some conversion. The Babel tooling comes with a config file
.babelrc which we use to tell it to build the JSX and React components:
index.html and put it in
src/main/resources/static/index.js. It’s almost a copy paste, but we will want to add the CSS imports:
and the imports from React look like this:
You can build that with
npm run build (or
./mvnw generate-resources) and it should work - all the tabs have some content and all the buttons generate some content.
Finally we just need to tidy up the
index.html so that it only imports the
index.css, and then all the features from the Webjars project should be working as expected.
There are many choices available for client side development, and Spring Boot doesn’t really have much influence on any of them, so you are free to choose whatever suits you. This article was necessarily limited in scope (we literally can’t look at everything from every angle), but hopefully was able to highlight some of the interesting possibilities. I find myself personally quite attached to HTMX having used it for a few mini projects recently, but your mileage, as ever, may vary. Please comment on the blog or send feedback via Github or the angry bird app - it will be interesting to hear what people think. Should we publish this article as a tutorial on spring.io for example?