Awesome
Spring Boot and Thymeleaf library for htmx
The project simplifies the integration of htmx with Spring Boot / Spring Web MVC applications. It provides a set of views, annotations, and argument resolvers for controllers to easily handle htmx-related request and response headers. This ensures seamless interaction between the frontend and backend, especially for dynamic content updates via htmx.
Additionally, the project includes a custom Thymeleaf dialect to enable smooth rendering of htmx-specific attributes within Thymeleaf templates. With these tools, developers can quickly implement htmx-driven interactions, such as AJAX-based partial page updates, with minimal configuration.
Maven configuration
The project provides the following libraries, which are available on Maven Central, so it is easy to add the desired dependency to your project.
htmx-spring-boot
Provides annotations and helper classes.
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot</artifactId>
<version>LATEST_VERSION_HERE</version>
</dependency>
htmx-spring-boot-thymeleaf
Provides a Thymeleaf dialect to easily work with htmx attributes.
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot-thymeleaf</artifactId>
<version>LATEST_VERSION_HERE</version>
</dependency>
Usage
Configuration
The included Spring Boot Auto-configuration will enable the htmx integrations.
Mapping Requests
Controller methods can be annotated with
HxRequest
to be selected only if it is a htmx-based request (e.g. hx-get
).
This annotation allows composition if you want to combine them.
For example, you can combine annotations to create a custom @HxGetMapping
.
The following method is called only if the request was made by htmx.
@HxRequest
@GetMapping("/users")
public String users() {
return "view";
}
In addition, if you want to restrict the invocation of a controller method to a specific triggering element, you can set HxRequest#value to the ID or name of the element. If you want to be explicit use HxRequest#triggerId or HxRequest#triggerName
@HxRequest("my-element")
@GetMapping("/users")
public String users() {
return "view";
}
If you want to restrict the invocation of a controller method to having a specific target element defined, use HxRequest#target
@HxRequest(target = "my-target")
@GetMapping("/users")
public String users() {
return "view";
}
Request Headers
To access the various htmx Request Headers in a controller method, you can use the class HtmxRequest as a controller method argument.
@HxRequest
@GetMapping("/users")
public String users(HtmxRequest htmxRequest) {
if (htmxRequest.isHistoryRestoreRequest()) {
// do something
}
return "view";
}
Response Headers
There are two ways to set htmx Response Headers in controller methods. The first is to use HtmxResponse
as controller method argument in combination with different Views e.g. HtmxRedirectView
as return value. The second is to use annotations, e.g. @HxTrigger
to set the necessary response headers. The first method is more flexible and allows you to dynamically set the response headers based on the request.
HtmxResponse and Views
Most of the htmx Response Headers can be set by using HtmxResponse as controller method argument, except for some control flow response headers such as HX-Redirect. For these response headers, you have to use a corresponding view as return value of the controller method.
- HtmxRedirectView - sets the HX-Redirect header to do a client-side redirect.
- HtmxLocationRedirectView - sets the HX-Location header to do a client-side redirect without reloading the whole page.
- HtmxRefreshView - sets the HX-Refresh header to do a client-side refresh of the current page.
Special view name prefixes
For these views, there is also a special view name handling if you prefer to return a view name instead of a view instance.
- Redirect URLs can be specified via
redirect:htmx:
, e.g.redirect:htmx:/path
, which causes htmx to perform a redirect to the specified URL. - Location redirect URLs can be specified via
redirect:htmx:location:
, e.g.redirect:htmx:location:/path
, which causes htmx to perform a client-side redirect without reloading the entire page. - A refresh of the current page can be specified using
refresh:htmx
.
@HxRequest
@PostMapping("/user/{id}")
public String user(@PathVariable Long id, @ModelAttribute @Valid UserForm form,
BindingResult bindingResult, RedirectAttributes redirectAttributes,
HtmxResponse htmxResponse) {
if (bindingResult.hasErrors()) {
return "user/form";
}
// update user ...
redirectAttributes.addFlashAttribute("successMessage", "User has been successfully updated.");
htmxResponse.addTrigger("user-updated");
return "redirect:htmx:/user/list";
}
Annotations
The following annotations can be used on controller methods to set the necessary response headers.
- @HxLocation
- @HxPushUrl
- @HxRedirect
- @HxRefresh
- @HxReplaceUrl
- @HxReselect
- @HxReswap
- @HxRetarget
- @HxTrigger
- @HxTriggerAfterSettle
- @HxTriggerAfterSwap
Note Please refer to the related Javadoc to learn more about the available options.
If you want htmx to trigger an event after the response is processed, you can use the annotation @HxTrigger
which sets the necessary response header HX-Trigger.
@HxRequest
@HxTrigger("userUpdated") // the event 'userUpdated' will be triggered by htmx
@GetMapping("/users")
public String users() {
return "view";
}
HTML Fragments
In Spring MVC, view rendering typically involves specifying one view and one model. However, in htmx a common capability is to send multiple HTML fragments that
htmx can use to update different parts of the page, which is called Out Of Band Swaps. Spring offers the ability to return
multiple HTML fragments using Collection<ModelAndView>
or FragmentsRendering
as return type of controller. Further information on this can be found in the
Spring Framework documentation under HTML Fragments.
@HxRequest
@GetMapping("/users")
public View users(Model model) {
model.addAttribute("users", userRepository.findAll());
model.addAttribute("count", userRepository.count());
return FragmentsRendering
.with("users/list")
.fragment("users/count")
.build();
}
Example with Collection<ModelAndView>
:
@HxRequest
@GetMapping("/users")
public Collection<ModelAndView> users() {
return List.of(
new ModelAndView("users/list", Map.of("users", userRepository.findAll())),
new ModelAndView("users/count", Map.of("count", userRepository.count()))
);
}
Exceptions
It is also possible to use HtmxRequest
and HtmxResponse
as method argument in handler methods annotated with @ExceptionHandler
.
@ExceptionHandler(Exception.class)
public String handleError(Exception ex, HtmxRequest htmxRequest, HtmxResponse htmxResponse) {
if (htmxRequest.isHtmxRequest()) {
htmxResponse.setRetarget("#error-message");
}
return "error";
}
It is also possible to add annotations on an exception handler to set response headers.
@ExceptionHandler(Exception.class)
@HxRetarget("#error-message")
public String handleError(Exception ex) {
return "error";
}
Spring Security
The library has an HxRefreshHeaderAuthenticationEntryPoint
that you can use to have htmx force a full page browser
refresh in case there is an authentication failure.
If you don't use this, then your login page might be appearing in place of a swap you want to do somewhere.
See htmx-authentication-error-handling
blog post for detailed information.
To use it, add it to your security configuration like this:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// probably some other configurations here
var entryPoint = new HxRefreshHeaderAuthenticationEntryPoint();
var requestMatcher = new RequestHeaderRequestMatcher("HX-Request");
http.exceptionHandling(configurer -> configurer.defaultAuthenticationEntryPointFor(entryPoint, requestMatcher));
return http.build();
}
Thymeleaf
Markup Selectors and HTML Fragments
The Thymeleaf integration for Spring supports the specification of a Markup Selector for views. The Markup Selector will be used for selecting the section of the template that should be processed, discarding the rest of the template. This is quite handy when it comes to htmx and for example Out Of Band Swaps, where you only have to return parts of your template.
The following example combines two partials via HtmxResponse
with a Markup Selector
that selects the fragment list
(th:fragment="list"
) and another that selects the
fragment count
(th:fragment="count") from the template users
.
@HxRequest
@GetMapping("/users")
public View users(Model model) {
model.addAttribute("users", userRepository.findAll());
model.addAttribute("count", userRepository.count());
return FragmentsRendering
.with("users :: list")
.fragment("users :: count")
.build();
}
This is also possible using Collection<ModelAndView
as return type:
@HxRequest
@GetMapping("/users")
public Collection<ModelAndView> test() {
return List.of(
new ModelAndView("users :: list", Map.of("users", userRepository.findAll())),
new ModelAndView("users :: count", Map.of("count", userRepository.count()))
);
}
Dialect
The Thymeleaf dialect has appropriate processors that enable Thymeleaf to perform calculations and expressions in htmx-related attributes.
See Attribute Reference for the related htmx documentation.
Note The
:
colon instead of the typical hyphen.
hx:get
: This is a Thymeleaf processing enabled attributehx-get
: This is just a static attribute if you don't need the Thymeleaf processing
For example, this Thymeleaf template:
<div hx:get="@{/users/{id}(id=${userId})}" hx-target="#otherElement">Load user details</div>
Will be rendered as:
<div hx-get="/users/123" hx-target="#otherElement">Load user details</div>
The Thymeleaf dialect has corresponding processors for most of the hx-*
attributes.
Please open an issue if something is missing.
Note Be careful about using
#
in the value. If you dohx:target="#mydiv"
, then this will not work as Thymeleaf uses the#
symbol for translation keys. Either usehx-target="#mydiv"
orhx:target="${'#mydiv'}"
Map support for hx:vals
The hx-vals attribute allows to add to the parameters that will be submitted with the AJAX request. The value of the attribute should be a JSON string.
The library makes it a bit easier to write such a JSON string by adding support for inline maps.
For example, this Thymeleaf expression:
<div hx:vals="${ {id: user.id } }"></div>
will render as:
<div hx-vals="{&quot;id&quot;: 1234 }"></div>
(Given user.id
has the value 1234
)
You can use multiple values like this:
<div hx:vals="${ {id: user.id, groupId: group.id } }"></div>
Articles
Links to articles and blog posts about this library:
- Redirect attributes with Spring MVC and htmx
- Release 1.0.0 and 2.0.0 of htmx-spring-boot-thymeleaf
- Htmx authentication error handling
- Thymeleaf and htmx with out of band swaps
Spring Boot compatibility
Library version | Spring Boot | Minimum Java version | Documentation |
---|---|---|---|
4.0.0 | 3.4.x | 17 | README.md |
3.6.2 | 3.2.x | 17 | README.md |
3.5.1 | 3.2.x | 17 | README.md |
3.4.1 | 3.2.x | 17 | README.md |
3.3.0 | 3.1.x | 17 | README.md |
3.2.0 | 3.1.x | 17 | README.md |
2.2.0 | 3.0.x | 17 | README.md |
1.0.0 | 2.7.x | 11 | README.md |
Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
License
Release
To release a new version of the project, follow these steps:
- Update
pom.xml
with the new version (Usemvn versions:set -DgenerateBackupPoms=false -DnewVersion=<VERSION>
) - Commit the changes locally.
- Tag the commit with the version (e.g.
1.0.0
) and push the tag. - Create a new release in GitHub via https://github.com/wimdeblauwe/htmx-spring-boot/releases/new
- Select the newly pushed tag
- Update the release notes. This should automatically start the release action.
- Update
pom.xml
again with the nextSNAPSHOT
version. - Close the milestone in the GitHub issue tracker.