neuron-explorer
Qwen3.5‑4B 전용 조향형 뉴런 탐색기, 모든 MLP 뉴런을 밀어 결과를 보관·탐색
Spring Boot와 Thymeleaf용 htmx 헬퍼
Spring Boot and Thymeleaf helpers for working with htmx
개발 도구Java · HTMLGitHub @wimdeblauwe직접 설치운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗📕 Want to write production-ready Spring Boot starters like this one? Read Crafting Spring Boot Starters.
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.
ℹ️ For htmx 2.x support, please refer to the 5.x branch.
The project provides the following libraries, which are available on Maven Central, so it is easy to add the desired dependency to your project.
Provides annotations and helper classes.
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot</artifactId>
<version>LATEST_VERSION_HERE</version>
</dependency>
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>
The included Spring Boot Auto-configuration will enable the htmx integrations.
The JsonMapper used to serialize JSON values written to htmx response headers (e.g. HX-Trigger)
can be customized by overriding the bean named htmxJsonMapper:
@Bean
JsonMapper htmxJsonMapper() {
return JsonMapper.builder()
// header values must be ASCII, so keep escaping non-ASCII characters
.enable(JsonWriteFeature.ESCAPE_NON_ASCII)
// your customizations
.build();
}
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";
}
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";
}
The HtmxRequest object
is also exposed as a model attribute named htmxRequest, so you can access it directly in your views, such as Thymeleaf templates.
<div th:if="${htmxRequest.isHtmxRequest()}">It is an htmx Request</div>
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.
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.
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:htmx:, e.g. redirect:htmx:/path, which causes htmx to perform a redirect to the specified URL.redirect:htmx:location:, e.g. redirect:htmx:location:/path, which causes htmx to perform a client-side redirect without reloading the entire page.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";
}
The following annotations can be used on controller methods to set the necessary response headers.
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";
}
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()))
);
}
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 answers requests with redirects and error pages that are meant for the browser, not for htmx. The library has classes for two situations:
Spring Security has one authentication entry point and one access denied handler per filter chain, so pick one of the approaches below for those two. The login, logout and failure handlers of Signing in with htmx can be combined with any of them.
| Entry point | What an htmx request gets when authentication is needed |
|---|---|
HxRedirectToPageAuthenticationEntryPoint | HX-Redirect to the page: a normal navigation to it, after which your own entry point asks the user to sign in (recommended) |
HxRefreshHeaderAuthenticationEntryPoint | HX-Refresh: the current page is reloaded |
HxLocationRedirectAuthenticationEntryPoint | HX-Location to a fixed URL: htmx loads that page with an ajax request and swaps it into the body |
When the session has expired, an htmx request gets Spring Security's normal answer, which htmx cannot follow: a GET is redirected to the login page, which htmx swaps into the request's target, and a POST (or PUT, PATCH, DELETE) fails its CSRF check with a 403, which htmx does not swap, so nothing happens on the page. On top of that, the htmx request's URL is saved as the request to return to after signing in, and it may be a fragment endpoint.
HxRedirectToPageAuthenticationEntryPoint and HxRedirectToPageAccessDeniedHandler answer such htmx requests with
HX-Redirect to the page the request was made from (HX-Current-URL, or the link itself for a boosted link).
The browser loads that page as a normal navigation, so your own entry point takes over (a login form, an OAuth2
provider) and the page is what the user returns to after signing in:
Give the entry point the one your application uses for every other request, such as the login form's:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// probably some other configurations here
http.formLogin(login -> login.loginPage("/login").permitAll())
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new HxRedirectToPageAuthenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")))
.accessDeniedHandler(new HxRedirectToPageAccessDeniedHandler()));
return http.build();
}
With OAuth2 login, that is new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/<registration id>").
HxRedirectToPageAccessDeniedHandler as its delegate.RequestCache. Pass your own cache to its constructor if it is
not an HttpSessionRequestCache.AccessDeniedHandlerImpl by default).HX-Current-URL are used, so the redirect stays on your application's origin. Without a
usable page, HX-Refresh reloads the current one.defaultAuthenticationEntryPointFor(entryPoint, new RequestHeaderRequestMatcher("HX-Request"))? Spring
Security makes the first entry point registered that way the default for requests no other entry point claims, and
that registration runs before the login form's. Requests that are no htmx requests and do not ask for HTML (a
fetch without an Accept header, curl, a MockMvc test without accept(...)) would then get a plain 401 (the
entry point's no-argument constructor) instead of the redirect to the login page.The older HxRefreshHeaderAuthenticationEntryPoint is a simpler alternative for the entry point: it answers with a
403 and HX-Refresh, so htmx reloads the current page. It leaves the htmx request saved as the request to return to,
a boosted link reloads the page the user was on rather than the one they clicked, and it does not cover a POST whose
CSRF token expired with the session.
See the htmx-authentication-error-handling
blog post for the background.
var htmxRequest = new RequestHeaderRequestMatcher("HX-Request");
http.exceptionHandling(exceptions -> exceptions
.defaultAuthenticationEntryPointFor(new HxRefreshHeaderAuthenticationEntryPoint(), htmxRequest));
htmx provides a special way to send a redirect instruction to the client, keeping a success code (200) and sending a custom HTTP header from the server (HX-Location / HX-Redirect). Htmx correctly interprets these headers and follows the redirect, replacing the response in the page body.
You can take advantage of this behavior by integrating the HxLocationRedirectAuthenticationFailureHandler, HxLocationRedirectAuthenticationSuccessHandler, HxLocationRedirectLogoutSuccessHandler, HxLocationRedirectAuthenticationEntryPoint and/or HxLocationRedirectAccessDeniedHandler into the SecurityFilterChain bean definition.
For non-htmx requests, they redirect as usual.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// probably some other configurations here
return http
.formLogin(login -> login
.failureHandler(new HxLocationRedirectAuthenticationFailureHandler("/login?failure"))
.successHandler(new HxLocationRedirectAuthenticationSuccessHandler("/home?login"))
).logout(logout -> logout
.logoutSuccessHandler(new HxLocationRedirectLogoutSuccessHandler("/home?logout"))
).exceptionHandling(handler -> handler
.authenticationEntryPoint(new HxLocationRedirectAuthenticationEntryPoint("/login?unauthorized"))
.accessDeniedHandler(new HxLocationRedirectAccessDeniedHandler("/error?forbidden"))
).build();
}
HxLocationRedirectAuthenticationEntryPoint and HxLocationRedirectAccessDeniedHandler take the place of
the classes for an expired session: there is one entry point and one access denied
handler. To sign in with htmx and handle an expired session, combine the login, logout and failure handlers with those
instead:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// probably some other configurations here
return http
.formLogin(login -> login
.loginPage("/login")
.permitAll()
.failureHandler(new HxLocationRedirectAuthenticationFailureHandler("/login?failure"))
.successHandler(new HxLocationRedirectAuthenticationSuccessHandler("/home?login"))
).logout(logout -> logout
.logoutSuccessHandler(new HxLocationRedirectLogoutSuccessHandler("/home?logout"))
).exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new HxRedirectToPageAuthenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")))
.accessDeniedHandler(new HxRedirectToPageAccessDeniedHandler())
).build();
}
The success handler sends the user back to the request Spring Security saved, which is then the page, and to
/home?login when nothing was saved.
Also, you can use the provided HxLocationBoostedRedirectStrategy as the second parameter in the handlers, instructing the client to include the HX-Boosted header in the new request. This can be useful if you want to take advantage of existing controller optimizations, for example, rendering a fragment instead of the full page for non-boosted, htmx-driven requests:
@GetMapping("/login")
String login(HtmxRequest request) {
return request.isHtmxRequest() && !request.isBoosted()
? "pages/login :: content"
: "pages/login";
}
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()))
);
}
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 processingFor 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 core hx-* attributes, as well as for those from the official extensions.
However, if you need an attribute that is not supported out of the box (e.g. from a custom extension), you can register it by customizing the dialect through an HtmxDialectCustomizer bean:
@Bean
HtmxDialectCustomizer htmxDialectCustomizer() {
return dialect -> dialect
.registerCustomAttribute("my-attribute")
.registerCustomCsrfAttribute("my-csrf-attribute")
.registerCustomCsrfAttribute("my-csrf-on-post-attribute", value -> value.equals("post"));
}
my-attribute as a custom attribute. You can use hx:my-attribute="${someValue}" in the Thymeleaf template, and it is rendered as hx-my-attribute.my-csrf-attribute as a CSRF-aware attribute. You can use hx:my-csrf-attribute="${someValue}" in the Thymeleaf template, and the CSRF token is automatically rendered into the hx-headers attribute of the HTML element.my-csrf-on-post-attribute as a conditional CSRF-aware attribute. You can use hx:my-csrf-on-post-attribute="${someValue}" in the Thymeleaf template, and the CSRF token is automatically rendered into the hx-headers attribute of the HTML element, but only if the value of the attribute is post.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'}"
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>
A Cross-Site Request Forgery (CSRF) attack tricks an authenticated user into performing unintended state-changing actions in a web application.
By default, Spring Security provides built-in protection against CSRF attacks on unsafe HTTP methods, while Thymeleaf automatically includes the required CSRF token as a hidden field in forms using th:action.
The library extends this support to htmx by automatically injecting the CSRF token into the request headers through the hx-headers attribute of elements using hx:post, hx:put, hx:patch, or hx:delete, even if the element is not part of a form.
Links to articles and blog posts about this library:
| Library version | htmx | Spring Boot | Minimum Java version | Documentation |
|---|---|---|---|---|
| 6.0.1 | 4.x | 4.1.x | 17 | README.md |
| 5.2.0 | 1.x, 2.x | 4.0.8 | 17 | README.md |
| 5.1.1 | 1.x, 2.x | 4.0.8 | 17 | README.md |
| 5.1.0 | 1.x, 2.x | 4.0.3 | 17 | README.md |
| 5.0.0 | 1.x, 2.x | 4.0.0 | 17 | README.md |
| 4.0.3 | 1.x, 2.x | 3.4.x, 3.5.x | 17 | README.md |
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.
To release a new version of the project, follow these steps:
pom.xml with the new release version.mvn versions:set -DgenerateBackupPoms=false -DnewVersion=<VERSION>README.md.Release <VERSION>.1.0.0) and push the tag.pom.xml again with the next SNAPSHOT version.| 3.6.2 |
| 1.x, 2.x |
| 3.2.x |
| 17 |
| README.md |
| 3.5.1 | 1.x, 2.x | 3.2.x | 17 | README.md |
| 3.4.1 | 1.x, 2.x | 3.2.x | 17 | README.md |
| 3.3.0 | 1.x | 3.1.x | 17 | README.md |
| 3.2.0 | 1.x | 3.1.x | 17 | README.md |
| 2.2.0 | 1.x | 3.0.x | 17 | README.md |
| 1.0.0 | 1.x | 2.7.x | 11 | README.md |