htmx-spring-boot

repository·main·Indexed 20 days ago

https://github.com/wimdeblauwe/htmx-spring-boot

A library bridging Spring Boot/Spring Web MVC and htmx. It provides tools to handle htmx-specific headers and attributes via annotations like @HxRequest, argument resolvers such as HtmxRequest and HtmxResponse, and a custom Thymeleaf dialect for rendering htmx attributes and fragments. It includes specialized handlers for Spring Security to manage authentication failures and redirects in htmx requests, as well as support for Out Of Band (OOB) swaps.

Tokens
2.6K
Snippets
10
Records
14
Agent score
22%

What's inside htmx-spring-boot

  1. Automatic CSRF token injection for htmx

    main
    The library automatically injects CSRF tokens into htmx request headers via the hx-headers attribute. This works for elements using hx:post, hx:put, hx:patch, or hx:delete, even if the element is not part of a standard HTML form.
  2. Configure Spring Security for htmx

    main

    Standard Spring Security redirects (like a 302 to a login page) can break htmx swaps by replacing the entire page content with the login form.

    Force Full Page Refresh on Auth Failure

    Use HxRefreshHeaderAuthenticationEntryPoint to force a full browser refresh when authentication fails, ensuring the user sees the login page correctly.

    Use Client-Side Redirects (HX-Location/HX-Redirect)

    To maintain a successful HTTP status (e.g., 200 OK) while instructing htmx to redirect, use the provided specialized handlers:

    • HxLocationRedirectAuthenticationFailureHandler
    • HxLocationRedirectAuthenticationSuccessHandler
    • HxLocationRedirectLogoutSuccessHandler
    • HxLocationRedirectAuthenticationEntryPoint
    • HxLocationRedirectAccessDeniedHandler
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        var entryPoint = new HxRefreshHeaderAuthenticationEntryPoint();
        var requestMatcher = new RequestHeaderRequestMatcher("HX-Request");
        http.exceptionHandling(configurer -> configurer.defaultAuthenticationEntryPointFor(entryPoint, requestMatcher));
        return http.build();
    }
  3. Use hx: attributes and inline maps in Thymeleaf

    main

    The library provides a Thymeleaf dialect that enables htmx-specific processing using the hx: prefix (instead of the static hx- prefix).

    hx: attributes

    Use hx:get, hx:post, etc., to allow Thymeleaf to evaluate expressions within htmx attributes. Example: <div hx:get="@{/users/{id}(id=${userId})}"> will render a valid htmx URL.

    hx:vals with inline maps

    You can write JSON for hx-vals more easily using Thymeleaf inline maps: Example: <div hx:vals="${ {id: user.id, groupId: group.id } }">

    Warning: When using hx:target, avoid using the # symbol directly in the Thymeleaf expression (e.g., hx:target="#mydiv") as Thymeleaf interprets # as a translation key. Instead, use hx-target="#mydiv" or hx:target="${'#mydiv'}".

    <div hx:get="@{/users/{id}(id=${userId})}" hx-target="#otherElement">Load user details</div
  4. Install htmx-spring-boot-thymeleaf via Maven

    main

    To enable the custom Thymeleaf dialect for working with htmx attributes in your templates, add the following dependency to your pom.xml:

    <dependency>
        <groupId>io.github.wimdeblauwe</groupId>
        <artifactId>htmx-spring-boot-thymeleaf</artifactId>
        <version>LATEST_VERSION_HERE</version>
    </dependency>
  5. Advanced htmx and Spring MVC patterns

    main

    The following resources provide guidance on specific integration patterns between htmx and Spring MVC/Thymeleaf:

    • Redirecting with attributes: How to handle Spring MVC redirect attributes when working with htmx.
    • Authentication error handling: Best practices for managing htmx authentication errors.
    • Out-of-band (OOB) swaps: Using Thymeleaf to implement htmx out-of-band swaps.
  6. Use Thymeleaf Markup Selectors for htmx fragments

    main

    The Thymeleaf integration allows you to use Markup Selectors (e.g., template :: fragment) in your view names. This allows you to return only a specific part of a template, which is ideal for htmx updates and OOB swaps.

    Example: Returning the list and count fragments from the users template.

    @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();
    }
  7. Install htmx-spring-boot via Maven

    main

    To use the core annotations and helper classes for handling htmx requests and responses in your Spring Boot application, add the following dependency to your pom.xml:

    <dependency>
        <groupId>io.github.wimdeblauwe</groupId>
        <artifactId>htmx-spring-boot</artifactId>
        <version>LATEST_VERSION_HERE</version>
    </dependency>
  8. Set htmx response headers using HtmxResponse or Annotations

    main

    There are two primary ways to set htmx response headers:

    1. Using HtmxResponse (Dynamic)

    Inject HtmxResponse as a controller argument to dynamically set headers. This is the most flexible method.

    2. Using Annotations (Static)

    Use annotations for fixed header values. Common annotations include:

    • @HxTrigger: Sets the HX-Trigger header to trigger an event in htmx.
    • @HxReselect, @HxReswap, @HxRetarget: For controlling swap behavior.
    • @HxPushUrl, @HxReplaceUrl: For URL manipulation.

    3. Using Special View Names (Redirects/Refresh)

    Instead of returning a View instance, you can return a string with specific prefixes:

    • redirect:htmx:/path: Performs a client-side redirect (HX-Redirect).
    • redirect:htmx:location:/path: Performs a client-side redirect without reloading the whole page (HX-Location).
    • refresh:htmx: Refreshes the current page (HX-Refresh).
    @HxRequest
    @HxTrigger("userUpdated")
    @GetMapping("/users")
    public String users() {
        return "view";
    }
  9. Return multiple HTML fragments (Out Of Band Swaps)

    main

    To support htmx Out Of Band (OOB) swaps, you can return multiple HTML fragments from a single controller method. This is achieved by returning either a Collection<ModelAndView> or using the FragmentsRendering builder.

    Using FragmentsRendering

    This is the recommended way to build a collection of fragments to be sent in one response.

    @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();
    }
  10. Access htmx request headers with HtmxRequest

    main

    To access specific htmx request headers (like HX-Trigger, HX-Request, etc.) within your controller, include HtmxRequest as a method argument. This allows you to inspect the request state, such as whether it is a history restore request.

    @HxRequest
    @GetMapping("/users")
    public String users(HtmxRequest htmxRequest) {
        if (htmxRequest.isHistoryRestoreRequest()) {
            // do something
        }
        return "view";
    }
  11. Restrict controller methods to htmx requests using @HxRequest

    main

    Use the @HxRequest annotation to ensure a controller method is only invoked if the request was made via htmx (e.g., via hx-get, hx-post, etc.).

    Restricting by Trigger Element

    You can restrict invocation to a specific triggering element by providing its ID or name to the @HxRequest value. For explicit control, use triggerId or triggerName:

    • @HxRequest("my-element") (matches ID or name)
    • @HxRequest(triggerId = "my-id")
    • @HxRequest(triggerName = "my-name")

    Restricting by Target Element

    Use target to restrict invocation to requests targeting a specific element:

    • @HxRequest(target = "my-target")
    @HxRequest
    @GetMapping("/users")
    public String users() {
        return "view";
    }