Giter Club home page Giter Club logo

htmx-spring-boot's Introduction

Discord Maven Central javadoc

Spring Boot and Thymeleaf library for htmx

This project provides annotations, helper classes and a Thymeleaf dialect to make it easy to work with htmx in a Spring Boot application.

More information about htmx can be viewed on their website.

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 controller methods to htmx 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 htmxRequest(){
    return "partial";
}

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 htmxRequest(){
    return "partial";
}

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 htmxRequest(){
    return "partial";
}

Using HtmxRequest to access HTTP request headers sent by htmx

The HtmxRequest object can be used as controller method parameter to access the various htmx Request Headers.

@HxRequest
@GetMapping("/users")
public String htmxRequest(HtmxRequest htmxRequest) {
    if(htmxRequest.isHistoryRestoreRequest()){
        // do something
    }
    return "partial";
}

Response Headers

There are two ways to set htmx Response Headers on controller methods. The first is to use annotations, e.g. @HxTrigger, and the second is to use the class HtmxResponse as the return type of the controller method.

See Response Headers Reference for the related htmx documentation.

The following annotations are currently supported:

Note Please refer to the related Javadoc to learn more about the available options.

Examples

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 hxUpdateUser(){
    return "partial";
}

If you want to do the same, but in a more flexible way, you can use HtmxResponse as the return type in the controller method instead.

@HxRequest
@GetMapping("/users")
public HtmxResponse hxUpdateUser(){
    return HtmxResponse.builder()
        .trigger("userUpdated") // the event 'userUpdated' will be triggered by htmx
        .view("partial")
        .build();
}

Out Of Band Swaps

htmx supports updating multiple targets by returning multiple partials in a single response, which is called Out Of Band Swaps. For this purpose, use HtmxResponse as the return type of a controller method, where you can add multiple templates.

@HxRequest
@GetMapping("/partials/main-and-partial")
public HtmxResponse getMainAndPartial(Model model){
    model.addAttribute("userCount", 5);
    return HtmxResponse.builder()
        .view("users-list")
        .view("users-count")
        .build();
}

An HtmxResponse can be formed from view names, as above, or fully resolved View instances, if the controller knows how to do that, or from ModelAndView instances (resolved or unresolved). For example:

@HxRequest
@GetMapping("/partials/main-and-partial")
public HtmxResponse getMainAndPartial(Model model){
    return HtmxResponse.builder()
        .view(new ModelAndView("users-list")
        .view(new ModelAndView("users-count", Map.of("userCount",5))
        .build();
}

Using ModelAndView means that each fragment can have its own model (which is merged with the controller model before rendering).

Error handlers

It is possible to use HtmxResponse as a return type from error handlers. This makes it quite easy to declare a global error handler that will show a message somewhere whenever there is an error by declaring a global error handler like this:

@ExceptionHandler(Exception.class)
public HtmxResponse handleError(Exception ex) {
    return HtmxResponse.builder()
                       .reswap(HtmxReswap.none())
                       .view(new ModelAndView("fragments :: error-message", Map.of("message", ex.getMessage())))
                       .build();
}

This will override the normal swapping behaviour of any htmx request that has an exception to avoid swapping to occur. If the error-message fragment is declared as an Out Of Band Swap and your page layout has an empty div to "receive" that piece of HTML, then only that will be placed on the screen.

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(exception ->
        exception.defaultAuthenticationEntryPointFor(entryPoint, requestMatcher));
    return http.build();
}

Thymeleaf

Markup Selectors and Out Of Band Swaps

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("/partials/main-and-partial")
public HtmxResponse getMainAndPartial(Model model){
    model.addAttribute("userCount", 5);
    return HtmxResponse.builder()
        .view("users :: list")
        .view("users :: count")
        .build();
}

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 attribute
  • hx-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 do hx:target="#mydiv", then this will not work as Thymeleaf uses the # symbol for translation keys. Either use hx-target="#mydiv" or hx: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="{&amp;quot;id&amp;quot;: 1234 }"></div>

(Given user.id has the value 1234)

Articles

Links to articles and blog posts about this library:

Spring Boot compatibility

Library version Spring Boot Minimum Java version
3.2.0 3.1.x 17
2.2.0 3.0.x 17
1.0.0 2.7.x 11

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

Apache 2.0

Release

To release a new version of the project, follow these steps:

  1. Update pom.xml with the new version (Use mvn versions:set -DgenerateBackupPoms=false -DnewVersion=<VERSION>)
  2. Commit the changes locally.
  3. Tag the commit with the version (e.g. 1.0.0) and push the tag.
  4. 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.
  5. Update pom.xml again with the next SNAPSHOT version.
  6. Close the milestone in the GitHub issue tracker.

htmx-spring-boot's People

Contributors

checketts avatar credmond avatar dertobsch avatar dsyer avatar harsh4902 avatar johannesg avatar klu2 avatar nathan-melaku avatar odrotbohm avatar ohgillwhan avatar riggs333 avatar runek00 avatar sullis avatar tk-png avatar waldyrious avatar wimdeblauwe avatar xhaggi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

htmx-spring-boot's Issues

Add HxPush annotation

We have a working HxPush annotation using an Interceptor, should I create a PR?

@Repeatable(HxPushs.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@HxRequest
public @interface HxPush {

  String url() default "";

  String urlPrefix() default "";

  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  @interface HxPushs {

    HxPush[] value();
  }
}
public class HtmxPushUrlInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(
      @NotNull HttpServletRequest request,
      @NotNull HttpServletResponse response,
      @NotNull Object handler) {
    if (handler instanceof HandlerMethod handlerMethod) {
      var hxPushUrls =
          AnnotatedElementUtils.findMergedRepeatableAnnotations(
              handlerMethod.getMethod(), HxPush.class, HxPush.HxPushs.class);
      if (CollectionUtils.isNotEmpty(hxPushUrls)) {
        var refererPath =
            String.valueOf(request.getHeader("referer"))
                .replace(String.valueOf(request.getHeader("origin")), "");
        var matchingPrefix =
            hxPushUrls.stream()
                .filter(
                    url ->
                        Strings.isNotEmpty(url.urlPrefix())
                            && refererPath.startsWith(url.urlPrefix()))
                .findFirst();
        var withoutPrefix =
            hxPushUrls.stream().filter(url -> Strings.isEmpty(url.urlPrefix()))
                .findFirst();
        if (matchingPrefix.isPresent()) {
          addHeader(request, response, matchingPrefix.get());
          return true;
        }
        withoutPrefix.ifPresent((prefix) -> addHeader(request, response, prefix));
        return true;
      }
    }
    return true;
  }

  private void addHeader(
      @NotNull HttpServletRequest request,
      @NotNull HttpServletResponse response,
      HxPush hxPushUrl) {
    if (Strings.isBlank(hxPushUrl.url())) {
      if (request.getQueryString() != null) {
        response.addHeader("HX-Push-Url",
            request.getServletPath() + "?" + request.getQueryString());
        return;
      }
      response.addHeader("HX-Push-Url", request.getServletPath());
    } else {
      response.addHeader("HX-Push-Url", hxPushUrl.url());
    }
  }
}

Support for Spring Boot 3 (Spring 6)

I was trying out this library with the current Spring Boot 3 milestone and got this error. The reason is that the library uses imports from org.thymeleaf.spring5 and these don't exist as a dependency in Spring Boot 3 anymore. Instead, the imports should be made from org.thymeleaf.spring6.

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'io.github.wimdeblauwe.hsbt.mvc.HtmxMvcConfiguration': Resolution of declared constructors on bean Class [io.github.wimdeblauwe.hsbt.mvc.HtmxMvcConfiguration] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@1de0aca6] failed
	...
Caused by: java.lang.NoClassDefFoundError: org/thymeleaf/spring5/view/ThymeleafViewResolver
	...

Remove commented out spring-asciidoctor-backends in pom.xml?

I noticed this commented out snippet in the pom.xml about spring-asciidoctor-backends:

htmx-spring-boot/pom.xml

Lines 151 to 160 in 46c13f1

<!-- Should be removed once spring-asciidoctor-backends is in Maven Central -->
<!--
<pluginRepositories>
<pluginRepository>
<id>spring-milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
-->

(I don't know yet what was planned to do with it (as I haven't found any asciidoc-files in this repository), maybe you wanted to create some documentation in spring style?)

Nevertheless, it looks like the artifact was published in January 2023 and is available in Maven Central.

If I misunderstood something and you want to keep the code in the pom.xml then feel free to close this issue.

@HxTrigger & @ResponseBody cannot work together (due to postHandle())

Hi -- firstly, great library -- thank you!

I have noticed an issue, however. I'm using a fairly typical Spring Boot + Thymeleaf setup, using Spring Boot 3.1.0-M1.

As an example:

    @HxTrigger("this-never-gets-set")
    @GetMapping("/cormac")
    @ResponseBody
    public String cormac(Model model, HttpServletResponse response) {
        response.setHeader("this-will", "work-okay");
        return "irrelevant";
    }

I am showing via code where I can set a header manually (i.e., this will work), but HxTrigger does NOT ultimately manage to set a header. It does try though (so that's fine), but, down the stack the header setting gets ignored because the request is actually committed already, and so the final response.setHeader(...) (after all the wrappers) doesn't ultimately get called. E.g., isCommitted() is true here in org.apache.catalina.connector.ResponseFacade's:

    @Override
    public void setHeader(String name, String value) {
        if (isCommitted()) {
            return;
        }

        response.setHeader(name, value);
    }

Ultimately, if you keep following this, you can see that the underlying Response.java's committed boolean is true.

HandlerInterceptorAdapters will not work with @ResponseBody and ResponseEntity methods because they are handled by StringHttpMessageConverter, which writes to and commits the response before postHandle(...) is ever called, which basically makes it impossible to change the response (although, it doesn't complain -- stuff just silently gets ignored).

I do notice you have a @ResponseBody test, but I think without a more realistic Spring context setup, you wouldn't see this issue in a what's basically unit test.

Check here, some people chatting about very similar issues:

spring-projects/spring-framework#13864
https://stackoverflow.com/questions/48823794/spring-interceptor-doesnt-add-header-to-restcontroller-services

So, I think preHandle(...) is the way to go and your use-case (just checking for annotated methods) is pretty simple, so I don't see any downsides to it or any need to do this in postHandle(...).

Update: I opened a PR with the suggested change.

Support automatically adding CSRF token when using the custom processors

When submitting forms, Thymeleaf's th:action attribute adds required CSRF tokens automatically. It seems to me that this feature isn't supported when using, for example hx:post on a form.

For reference, it would eliminate the need to manually add <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> to all of my newly-converted-to htmx forms.

Is this something that you envisage will be supported?

Preserve the order of templates inside HtmxResponse

The HtmxResponse currently stores its templates inside a Set, so the order is not preserved. Although stated differently in the docs of HTMX, the order might be relevant in some edge-cases, see bigskysoftware/htmx#1198.

I'd therefore suggest to change the implementeion to use a LinkedHashSet instead of a HashSet, it seems also more intuitive when debugging the generated responses.

adding multiple triggers causes exception

If you add multiple triggers to a HtmxResponse, like:

		htmxResponse.addTrigger("hyperlinkChanged");
		htmxResponse.addTrigger("hideModal");

Spring will throw a IllegalArgumentException (Spring boot 3.1).
As it seems a cr/lf is added instead of a coma to separate the different triggers:

java.lang.IllegalArgumentException: Invalid characters (CR/LF) in header HX-Trigger
	at org.springframework.util.Assert.isTrue(Assert.java:140) ~[spring-core-6.0.11.jar:6.0.11]
	at org.springframework.security.web.firewall.FirewalledResponse.validateCrlf(FirewalledResponse.java:76) ~[spring-security-web-6.1.2.jar:6.1.2]
	at org.springframework.security.web.firewall.FirewalledResponse.setHeader(FirewalledResponse.java:53) ~[spring-security-web-6.1.2.jar:6.1.2]
	at jakarta.servlet.http.HttpServletResponseWrapper.setHeader(HttpServletResponseWrapper.java:132) ~[tomcat-embed-core-10.1.11.jar:6.0]
	at jakarta.servlet.http.HttpServletResponseWrapper.setHeader(HttpServletResponseWrapper.java:132) ~[tomcat-embed-core-10.1.11.jar:6.0]
	at io.github.wimdeblauwe.hsbt.mvc.HtmxViewHandlerInterceptor.setTriggerHeader(HtmxViewHandlerInterceptor.java:120) ~[htmx-spring-boot-thymeleaf-2.1.0.jar:2.1.0]
	at io.github.wimdeblauwe.hsbt.mvc.HtmxViewHandlerInterceptor.addHxHeaders(HtmxViewHandlerInterceptor.java:85) ~[htmx-spring-boot-thymeleaf-2.1.0.jar:2.1.0]
	at io.github.wimdeblauwe.hsbt.mvc.HtmxViewHandlerInterceptor.postHandle(HtmxViewHandlerInterceptor.java:81) ~[htmx-spring-boot-thymeleaf-2.1.0.jar:2.1.0]

edit: i've realised I was one version behind. Just changed the stracktrace.

hx:vals not working with multiple key-values

While using hx:vals with one key value as mentioned in the docs it is working fine.

<button hx-post="/add-to-cart" hx:vals="${ {code: product.code} }">Add to Cart</button>

But, if I add more than one key-value pairs, it is throwing error as follows:

<button hx-post="/add-to-cart" hx:vals="${ {code: product.code} , {price: product.price} }">Add to Cart</button>
Caused by: org.attoparser.ParseException: Exception evaluating SpringEL expression: " {code: product.code} , {price: product.price} " (template: "partials/products" - line 35, col 37)
	at org.attoparser.MarkupParser.parseDocument(MarkupParser.java:393)
	at org.attoparser.MarkupParser.parse(MarkupParser.java:257)
	at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:230)
	... 52 more
Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: " {code: product.code} , {price: product.price} " (template: "partials/products" - line 35, col 37)
	at org.thymeleaf.spring6.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:292)
	at org.thymeleaf.standard.expression.VariableExpression.executeVariableExpression(VariableExpression.java:166)
....
....
....
Caused by: org.springframework.expression.spel.SpelParseException: Expression [ {code: product.code} , {price: product.price} ] @22: EL1041E: After parsing a valid expression, there is still more data in the expression: 'comma(,)'
	at org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression(InternalSpelExpressionParser.java:144)
	at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:63)
	at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:34)
	at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:56)

Just FYI: The following is working fine:

<button hx-post="/add-to-cart" th:hx-vals='|{"code":"${product.code}", "price":"${product.price}" |'>Add to Cart</button>

Unable to release version 3.3.0

I just tried to release a new version 3.3.0, but I keep running into issues with maven-gpg-plugin:

[INFO] --- maven-gpg-plugin:3.2.3:sign (sign-artifacts) @ htmx-spring-boot-parent ---
Warning:  
Warning:  W A R N I N G
Warning:  
Warning:  Do not store passphrase in any file (disk or SCM repository),
Warning:  instead rely on GnuPG agent or provide passphrase in 
Warning:  MAVEN_GPG_PASSPHRASE environment variable for batch mode.
Warning:  
Warning:  Sensitive content loaded from Mojo configuration
Warning:  
[INFO] Signer 'gpg' is signing 1 file with key default
gpg: no default secret key: No secret key
gpg: signing failed: No secret key
[INFO] --------------------------------------------------------------

I also tried to set the version explictly to 3.0.1 as there is a property in the pom.xml for it, but it was not used. But that gives the same error.

I did not change anything to the secrets that are available in Github on this project, so I don't know why this is now failing. If anybody has a clue, I would be happy to hear it.

Support returning multiple HX Trigger events

As I've begun using HTMX I've had some cases when I could choose to return multiple events, so I investigated the API. The following HX-Trigger headers could be set:

HX-Trigger: myEvent
HX-Trigger: {"myEvent": "my details"}
HX-Trigger: {"myEvent": "my details", "myEvent2": "my details"}

(along with the after-settle and after swap options)

To encapsulate these details I want to create an HtmxResponse object (combining @odrotbohm's HtmxPartials) with an api of:

htmxReponse.addTrigger("myEvent");
htmxReponse.addTrigger("myEvent", HxTriggerLifecycle.SWAP);
htmxReponse.addTrigger("myEvent", "myDetails");
htmxReponse.addTrigger("myEvent", "myDetails", HxTriggerLifecycle.SWAP);

Alternatively we could use a builder style, but I'm not sure if it offers much benefit:

htmxReponse.addTrigger("myEvent");
htmxReponse.addTrigger(new HxTriggerBuilder("myEvent").withDetails("myDetails").withLifeCycle(SWAP));

The shortest trigger option will be the most common (no details and the RECEIVE lifecycle)

That way users don't need to generate the json. I don't think we need to support all these options in the annotation.

Add Context Path to HtmxResponse browser redirect

If I have setup a context path on my application.yml
server.servlet.context-path: /bot-webapp if I return new HtmxResponse().browserRedirect("/") , it doesn't automatically add the context path. I have to manually add it to the return new HtmxResponse().browserRedirect("/bot-webapp")

Documentation

Is there a link to the full documentation? The Readme is not enough to get started.

Support for trigger name in `@HxRequest`

When you have a UI that consists of several fragments, that could each trigger an HTMX request, it would be nice if there was a way to disambiguate which element triggered the request without having to resort to additional URIs:

<div>
  <div hx-get="/" name="foo" th:fragment="foo"></div>
  <div hx-get="/" name="bar" th:fragment="bar"></div>
</div>

Let's say I have a controller and would like to handle these requests in separate controller methods, the only way would be to change the template to issue the request to different URIs that pollute the URI space. Alternatively, one would have to manually declare a header condition to the request mapping

@HxRequest
@GetRequest(path = "/", headers="HX-Trigger-Name=foo")
…

However, HTMX sends HX-Trigger-Name headers containing the element triggering the request with it. It would be nice if I could use that name in @HxRequest to constrain the mapping like this:

@Controller
class MyController {

  @HxRequest("foo") // Alternatively @HxRequest(trigger = "foo") or @HxRequest(triggerName = "foo")
  String foo() { … }

  @HxRequest("bar")
  String bar() { … }
}

As the trigger name is pretty core to identifying an HTMX request, I think it's reasonable to default the (still untaken) value attribute to it.

Add support for hx:vals for maps directly

hx-vals allows for json data to be added to a request. However that means it needs to be escaped with all the correct quotes like so:

hx:vals="${'{&quot;id&quot;:&quot;' + myId + '&quot;}'}"

which is rendered as: hx-vals="{'id': 1234}" but is very hard to write and read.

Thymeleaf supports inline maps like so:

hx:vals="${ {id: id } }"

which renders as hx-vals="{id: 1234}" (note the lack of quotes for the attribute name) Which is not proper JSON and htmx can't use it. I would like to propose adding support for rendering JSON (using Jackson) for hx-vals.

What do you think?

Spring Security filter chain config

The following piece of code, when added breaks my app doing oauth2 login. Is there possibly a way its added to the existing config that's auto configured from application.yml.

@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(exception ->
        exception.defaultAuthenticationEntryPointFor(entryPoint, requestMatcher));
    return http.build();
}

For example, when I prepend the code with

http.authorizeHttpRequests { registry -> registry.requestMatchers("/**").authenticated() }
			.oauth2Login(Customizer.withDefaults())

its starts to work again, but I'm not really sure if I'm still overriding some other defaults. (sorry about the kotlin syntax)

Unable to find 1.x within maven central

Trying to test both htmx-spring-boot and htmx-spring-boot-thymleaf.

        <dependency>
            <groupId>io.github.wimdeblauwe</groupId>
            <artifactId>htmx-spring-boot</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>io.github.wimdeblauwe</groupId>
            <artifactId>htmx-spring-boot-thymeleaf</artifactId>
            <version>1.0.0</version>
        </dependency>

Results: Could not find artifact io.github.wimdeblauwe:htmx-spring-boot:pom:1.0.0 in central (https://repo1.maven.org/maven2)

Spring boot Version 2.7.10, Java 17

spring boot 3.2.3: umlaut encoding issue?

Summary

I am experiencing an issue I only see with htmx responses that is why I open the issue here, but it might be a spring boot 3.2.3 issue.

After switching form spring boot 3.2.2 to 3.2.3 I see broken "umlauts".

Screenshot from 2024-02-23 17-49-30

Response header looks fine, content "looks" identical but the weird thing is that the content length is different, off by 1 B (3.2.3 has -1 B). Unsure if this is the issue.

Spring Boot

3.2.3

Details

3.2.3

HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Language: de
Content-Length: 214
Date: Fri, 23 Feb 2024 16:08:47 GMT
Keep-Alive: timeout=60
Connection: keep-alive

3.2.2

HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Language: de
Content-Length: 215
Date: Fri, 23 Feb 2024 16:14:01 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Remove System.out.println

HtmxHandlerMethodArgumentResolver has:

System.out.println("nativeRequest = " + nativeRequest);

which should be removed.

Add before/after code examples

I really liked the before/after comparison shown in your Devoxx presentation, starting at 39:33.

For example, for processors, instead of:

<button th:attr="hx-get=|/refresh-button/${device.id}|,
                 hx-target=|#device-info-${device.id}|,
                 hx-indicator=|#device-info-${device.id}|"
        hx-swap="outerHTML"
        class="...">
</button>

...the syntax is made cleaner with

<button hx:get="|/refresh-button/${device.id}|"
        hx:target="|#device-info-${device.id}|"
        hx:indicator="|#device-info-${device.id}|"
        hx-swap="outerHTML"
        class="...">
</button>

I think it would be nice to include an example or two of such before/after code in the README, to motivate usage of this tool.

hx:target throws an error when a raw CSS selector for ID is used: hx:target="#myId"

Since the # sign is used when looking up message properties, Thymeleaf throws an error when a CSS selector is used directly.

This works: hx:target="${'#myDiv'}
This also works: hx-target="#myDiv" (Since it isn't a thymeleaf processor)
This fails: hx:target="#myDiv"

However the doProcess method that is failing in a protected final method of the parent class, so we can't override it to handle this single case.

I think if I use a delegate object, we can handle the edge case for hx:target processor only. I'll look through the other processors to see if the ID use case is common enough to warrant this special case.

I do think it that we need to address it more than just documenting that it isn't supported since the purpose of the hx:target is to reference an id.

add reswap to HtmxResponse

htmx supports the HX-Reswap header to redefine the swap attribute.
Would be cool if HtmxResponse class would also support that.

Expose HtmxResponse internals for testing verification

I'm adding MockMvc tests for some of my Htmx based endpoints and it currently isn't possible to write an 'expects' block against HtmxResponse templates since all the getters are package private.

We could:
1- Make the getters public so callers could pull out internals to assert against them
2- Add a testing support library and matcher that is in the same package as HtmxResponse so it has access to the internals,

I favor option #2 but since the repo isn't setup for multiple modules, it seems like it would be a large refactor to add it in. Would you like me to attempt that?

Investigate hypermedia in MockMVC testing

HTMX's support for hypermedia client means that we could write tests that validate those calls.

I wonder if we could add test support that would inspect a response, locate an hx-get request, then make a new MockMVC call that can be further verified.

  • Would need to create a 'testing' library
  • Determine what syntax would make sense
  • Is this concept even useful?

This isn't urgent. But the idea seems really promising. Any investigation can just take place in this project's src/test to determine if it is useful.

Add support for HtmxRequest injection into controller

Similar to https://django-htmx.readthedocs.io/en/latest/middleware.html we can probably create a HtmxRequestDetails class that users of the library could inject into their controller methods. I am thinking something like this:

public String myControllerMethod(Model model, HtmxRequestDetails details) {
  if( details.isHtmxRequest()) {
   return "partial";
  }
  return "full";
}

This would be an alternative to having 2 methods, where one is annotated with @HxRequest (See #1).

But this HtmxRequestDetails object could have more properties:

  • isBoosted() (if HX-Boosted header is present in request)
  • getCurrentUrl() (HX-Current-URL)
  • isHistoryRestoreRequest (HX-History-Restore-Request)
  • getPromptResponse() (HX-Prompt)
  • getTarget() (HX-Target)
  • getTriggerId() (HX-Trigger)
  • getTriggerName() (HX-Trigger-Name)

Functional HTMX Endpoints

I propose adding a HtmxEndpoint Class that can be used to create HTTP Endpoints and can be directly called from Template Engines.

The Controller would look like this:

@Controller 
class ExampleController {
  public HtmxEndpoint<UserForm> createUserEndpoint = new HtmxEndpoint<>(
      "/createUser",
      HttpMethod.POST,
      this::createUser
  );

  private ModelAndView createUser(UserForm userForm) {
    return new ModelAndView("createUser", Map.of("createUserEndpoint", createUserEndpoint));
  }
}

The class would like this:

public class HtmxEndpoint<T> implements RouterFunction<ServerResponse> {

  private final String path;
  private final HttpMethod method;
  private final Supplier<ModelAndView> modelAndViewSupplier;
  private final Function<T, ModelAndView> function;

  ParameterizedTypeReference<T> requestType = new ParameterizedTypeReference<>() {
  };

  public HtmxEndpoint(String path, HttpMethod method, Function<T, ModelAndView> function) {
    this.path = path;
    this.method = method;
    this.function = function;
    this.modelAndViewSupplier = null;
  }

  public HtmxEndpoint(String path, HttpMethod method, Supplier<ModelAndView> modelAndViewSupplier) {
    this.path = path;
    this.method = method;
    this.modelAndViewSupplier = modelAndViewSupplier;
    this.function = null;
    this.requestType = null;
  }

  @NotNull
  @Override
  public Optional<HandlerFunction<ServerResponse>> route(@NotNull ServerRequest request) {
    RequestPredicate predicate = RequestPredicates.method(method).and(RequestPredicates.path(path));
    if (predicate.test(request)) {
      ModelAndView modelAndView = getBody(request);
      return Optional.of(
          req -> RenderingResponse.create(modelAndView.view())
              .modelAttribute(modelAndView.model())
              .build()
      );
    }
    return Optional.empty();
  }

  private ModelAndView getBody(ServerRequest req) {
    if (function == null) {
      return modelAndViewSupplier.get();
    }

    try {
      return function.apply(
          req.body(requestType)
      );
    } catch (ServletException | IOException e) {
      throw new RuntimeException(e);
    }
  }

  public String call() {
    return "hx-" + method.name().toLowerCase() + " =\"" + path + "\"";
  }

}

In the template you would call it like this in Thymeleaf:

<div th:hx=${createUserEndpoint}>
<div>

And this template would render like this:

<div hx-post="/createUser">
<div>

Of course, the HtmxEndpoint could be expanded to all the possible Htmx attributes.

The Endpoints would be scanned at Startup using Reflection and added to the Spring RouterFunction

 @Bean
  ApplicationRunner applicationRunner() {
    return args -> {
      applicationContext.getBeansWithAnnotation(Controller.class)
          .values().forEach(controller ->
              {
                List<Field> fieldList = Arrays.stream(controller.getClass().getDeclaredFields())
                    .filter(method -> method.getType() == HtmxEndpoint.class)
                    .toList();

                fieldList.forEach(field -> {
                  RouterFunction<?> function = (RouterFunction<?>) ReflectionUtils.getField(field, controller);
                  if(routerFunctionMapping.getRouterFunction() == null){
                    routerFunctionMapping.setRouterFunction(function);
                  }
                  RouterFunction<?> routerFunction = routerFunctionMapping.getRouterFunction().andOther(function);
                  routerFunctionMapping.setRouterFunction(routerFunction);
                });
              }
          );
    };
  }

@WebMvcTest needs META-INF configuration

There is a org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc.imports in this project so the tests work here, but it's not in the published jar files so users do not benefit from it. Since it's just a text file we could move it into src/main/resources and everyone would be happy?

Ver. 3.0.0 breaks with spring-boot 3.1.2

org.springframework.boot version '3.1.2'.

	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-undertow'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	implementation group: 'io.github.wimdeblauwe', name: 'htmx-spring-boot', version: '3.0.0'
	implementation group: 'io.github.wimdeblauwe', name: 'htmx-spring-boot-thymeleaf', version: '3.0.0'

Results in...

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.class]: Failed to instantiate [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping]: Factory method 'requestMappingHandlerMapping' threw exception with message: No qualifying bean of type 'org.springframework.web.servlet.ViewResolver' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier("viewResolver")}

...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.web.servlet.ViewResolver' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier("viewResolver")}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1824) ~[spring-beans-6.0.11.jar:6.0.11]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1383) ~[spring-beans-6.0.11.jar:6.0.11]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory$DependencyObjectProvider.getObject(DefaultListableBeanFactory.java:2014) ~[spring-beans-6.0.11.jar:6.0.11]
	at io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxMvcAutoConfiguration.addInterceptors(HtmxMvcAutoConfiguration.java:45) ~[htmx-spring-boot-3.0.0.jar:3.0.0]
	at org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite.addInterceptors(WebMvcConfigurerComposite.java:88) ~[spring-webmvc-6.0.11.jar:6.0.11]
	at org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration.addInterceptors(DelegatingWebMvcConfiguration.java:83) ~[spring-webmvc-6.0.11.jar:6.0.11]
	at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.getInterceptors(WebMvcConfigurationSupport.java:358) ~[spring-webmvc-6.0.11.jar:6.0.11]
	at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.initHandlerMapping(WebMvcConfigurationSupport.java:511) ~[spring-webmvc-6.0.11.jar:6.0.11]
	at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.requestMappingHandlerMapping(WebMvcConfigurationSupport.java:312) ~[spring-webmvc-6.0.11.jar:6.0.11]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:139) ~[spring-beans-6.0.11.jar:6.0.11]
	... 20 common frames omitted

Seems to work fine with...

implementation group: 'io.github.wimdeblauwe', name: 'htmx-spring-boot-thymeleaf', version: '2.0.0'

Adding HtmxUtils for better reference in Templates

In every project I'm doing I create a HtmxUtil class, I think this improves template readability, and could start establishing good practices.

For example having a URI method to put variables into a constant:

  public static String URI(String uriTemplate, Object... variables) {
    return new UriTemplate(uriTemplate).expand(variables).toString();
  }

and then in the controller

  public static final String GET_EDIT_USER_MODAL = "/save-user/modal/{uuid}";

  @GetMapping(GET_EDIT_USER_MODAL)
  public ViewContext editUserModal(@PathVariable UUID uuid) {
    return editUserComponent.render(uuid);
  }

then in the template

        <button hx-get="${URI(GET_EDIT_USER_MODAL,uuid)}"
                hx-target="#${MODAL_CONTAINER_ID}">
            <img src="/edit.svg">
        </button>

Here's an example:
https://github.com/tschuehly/htmx-spring-workshop/blob/lab-4/src/main/java/de/tschuehly/easy/spring/auth/htmx/HtmxUtil.java

Rethink annotation support for htmx response headers

This issue is triggered by the discussion at #79

I guess for the server-side implementation, we have to make up our minds at some point how to deal with the fact that HTMX has request and response triggers. The current @HxTrigger seems to cater to the latter, which, I think, is a bit odd as controller method annotations are primarily metadata to map the incoming request.

Trying to summarize the proposals:

Have single request and response related annotation

  • @HxRequest, with value, triggerId and triggerName parameters
  • @HxResponse, with parameters for each response trigger (so refresh, triggers, redirect, ...)

Positive:

  • Simple as there are only 2 annotations.

Negative:

  • More typing for the response headers compared to being able to just add @ResponseHxTrigger to a method for example.

Drop the annotations for response headers

We could just support returning a HtmxResponse object where the response headers are set and no longer offer the annotations. This will also make it clear that annotations are only for mapping the incoming request.

Negative:

  • A void method that just wants to add a response header now has to return a HtmxResponse instead of just adding an annotation.

Have individual response headers

If we do this, we should all start them with @ResponseHx... to make it clear that they are response headers.

I think we can do this kind of breaking change in a new 4.0 release no problem. We just have to think hard what is the best way forward. There is no clear winner for me, so all feedback is welcome.

Support HtmxResponse in error handlers

I would love to be able to use HtmxResponse as the return type for error handlers. This would allow to do something like this:

@ExceptionHandler(Exception.class)
public HtmxResponse handleError(Exception ex) {
  return HtmxResponse.builder()
      .reswap(HtmxReswap.none())
      .view(new ModelAndView("fragments/flashmessage :: oob-flashmessage-info", Map.of("message", ex.getMessage())))
      .build();
}

The idea is that there is a <div/> on each page that allows to show a general error message via OOB swap.

As a workaround, I currently do this:

@ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletResponse response, Exception ex) {
  response.setHeader("HX-Reswap", "none");
  return new ModelAndView("fragments/flashmessage :: oob-flashmessage-warning", Map.of("message", ex.getMessage()));
}

But it would be nicer if we could use HtmxResponse.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.