wimdeblauwe / htmx-spring-boot

Spring Boot and Thymeleaf helpers for working with htmx
Apache License 2.0
437 stars 41 forks source link

Add support for HTMX partials. #19

Closed odrotbohm closed 2 years ago

odrotbohm commented 2 years ago

Introduce HtmxPartials to provide a convenient API for controller methods to specify Thymeleaf fragments to be rendered to ultimately trigger different parts of the page updated. The abstraction is handled in a custom HandlerInterceptor that renders a view that triggers the rendering of the registered fragments individually.

I've stripped down a lot of stuff from what I previously had in Spring Playground. The server sent events support is completely gone for now as that allows a significantly simplified arrangement and less abstractions to introduce. Feel free to massage as you see fit.

wimdeblauwe commented 2 years ago

I am trying to understand how to exactly use this. I want to write a variant of the TodoMvc app I've build with htmx before. That one uses triggers to do an extra request to the server to update the item count on the page when an item is added or removed. I'd like to use OOB swaps to test the API this PR brings.

I currently have this:

    @PostMapping
    @HxRequest
    public String htmxAddTodoItem(TodoItemFormData formData,
                                  Model model) {
        TodoItem item = repository.save(new TodoItem(formData.getTitle(), false));
        model.addAttribute("item", toDto(item));

        return "fragments :: todoItem";
    }

I want to now return that same fragment + fragments :: active-items-count fragment as OOB. The fragment needs a single attribute numberOfActiveItems to be present to render properly.

I got started like this:

 return new HtmxPartials()
                .replace("active-items-count")
                .with("fragments :: active-items-count");

1) Where do I set the extra attribute (numberOfActiveItems)? Just put it on the injected Model instance? It would have been great if I could re-use the method public String htmxActiveItemsCount(Model model) I already had. Pseudo-code:

return new HtmxPartials().replace("active-items-count").with(this::htmxActiveItemsCount);

It could take the result String as the fragment name and it would need to somehow pass in the model. Not sure if this is even possible, but just wanted to put this out there to think about.

2) Where do I add the "main content" fragment? I need a way to also have the fragments :: todoItem returned.

checketts commented 2 years ago

@odrotbohm Thanks a ton for this contribution here are my findings, which I'm happy to tackle and contribute to your branch (if you don't mind)

Base change:

Out-of-band changes are typically added to a base change (note the div without an hx-swap-oob attribute:

<div id="alert" hx-swap-oob="true">User successfully updated</div>
<div>
 <span>User details</div>
</div>

Extension

There are a few partials that I expect will get updated frequently. Allowing the user to create helper functions for those partials would be helpful:

class MyHtmxPartials extend HtmxPartials {
  public void addAlert(String alertText) {
    // add partial and model the allows for alerting
  }
}

Allow not returning an HtmxPartial

The current partial are only appended based on the return type. Adding an annotation and model attribute will allow alternate options

OOB swap parameters

https://htmx.org/attributes/hx-swap-oob/ See:

any valid [hx-swap](https://htmx.org/attributes/hx-swap) value, followed by a colon, followed by a CSS selector

odrotbohm commented 2 years ago

I'd be also fine if you took this over into a feature branch in the main repo and take it from there.

What's the story behind the extensions? How are those helper functions supposed to work? Also, can you elaborate "Allow not returning an HtmxPartial"? That's nothing that needs explicit support as it works OOTB, doesn't it? Spring MVC renders fragments already if the view name template::fragment is returned.

checketts commented 2 years ago

Allow not returning an HtmxPartial

I mean to be able to have a ModelAndView as the the return value for a method, but still have the HtmxPartial processing to kick in. I'm not certain how useful it is. More just wondering if requiring the return value to trigger the processing is the best approach.

This isn't a requirement, just wanted to ensure it wasn't too fragile if a different approach is needed.

Extension

I was just trying to think of how a user might extend the partials, like the 'alert' in my example. The helper function would just bundle calls for setting the model and selecting the replace/template calls. I'll add a test that demonstrates it (and if it is useful)

checketts commented 2 years ago

I've switched the target branch from 'main' to feature/partials so I can iterate on it a little more. Thanks so much for the contribution @odrotbohm !