Skip to content

Commit

Permalink
Merge pull request #128 from wimdeblauwe/checketts/htmx-response
Browse files Browse the repository at this point in the history
Add support for processing HtmxResponse in the Model and as an Argument
  • Loading branch information
wimdeblauwe authored Aug 28, 2024
2 parents 69fb0e1 + 54fb71d commit b117795
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 5 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,35 @@ public HtmxResponse getMainAndPartial(Model model){

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

### HtmxResponse.Builder as an argument

An `HtmxReponse.Builder` can be injected as a controller method. This creates the parameter and adds it to the model,
allowing it to be used without requiring it be the method return value. This is useful when the return value is needed for
the template.

This allows for the following usage:

```java
@GetMapping("/endpoint")
public String endpoint(HtmxResponse.Builder htmxResponse, Model model) {
htmxResponse.trigger("event1");
model.addAttribute("aField", "aValue");
return "endpointTemplate";
}
```

For example the [JTE templating library](https://jte.gg/) supports statically typed templates and can be used like so:

```java
@GetMapping("/endpoint")
public JteModel endpoint(HtmxResponse.Builder htmxResponse) {
htmxResponse.trigger("event1");
String aField = "aValue";
return templates.endpointTemplate(aField);
}
```


### Error handlers

It is possible to use `HtmxResponse` as a return type from error handlers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,41 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;

public class HtmxHandlerInterceptor implements HandlerInterceptor {

private final ObjectMapper objectMapper;
private final HtmxResponseHandlerMethodReturnValueHandler htmxResponseHandlerMethodReturnValueHandler;

public HtmxHandlerInterceptor(ObjectMapper objectMapper) {
public HtmxHandlerInterceptor(ObjectMapper objectMapper, HtmxResponseHandlerMethodReturnValueHandler htmxResponseHandlerMethodReturnValueHandler) {
this.objectMapper = objectMapper;
this.htmxResponseHandlerMethodReturnValueHandler = htmxResponseHandlerMethodReturnValueHandler;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if(modelAndView != null) {
modelAndView.getModel().values().forEach(
value ->{
if(value instanceof HtmxResponse) {
buildAndRender((HtmxResponse) value, modelAndView, request, response);
} else if (value instanceof HtmxResponse.Builder) {
buildAndRender(((HtmxResponse.Builder) value).build(), modelAndView, request, response);
}
});
}
}

private void buildAndRender(HtmxResponse htmxResponse, ModelAndView mav, HttpServletRequest request, HttpServletResponse response) {
View v = htmxResponseHandlerMethodReturnValueHandler.toView(htmxResponse);
try {
v.render(mav.getModel(), request, response);
htmxResponseHandlerMethodReturnValueHandler.addHxHeaders(htmxResponse, response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,21 @@ public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper));
registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper, createHtmxReponseHandler()));
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new HtmxHandlerMethodArgumentResolver());
resolvers.add(new HtmxResponseHandlerMethodArgumentResolver());
}

@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
handlers.add(new HtmxResponseHandlerMethodReturnValueHandler(resolver.getObject(), locales, objectMapper));
handlers.add(createHtmxReponseHandler());
}

private HtmxResponseHandlerMethodReturnValueHandler createHtmxReponseHandler() {
return new HtmxResponseHandlerMethodReturnValueHandler(resolver.getObject(), locales, objectMapper);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class HtmxResponseHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(HtmxResponse.Builder.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HtmxResponse.Builder htmxResponseBuilder = HtmxResponse.builder();
if(mavContainer != null) {
mavContainer.addAttribute(htmxResponseBuilder);
}
return htmxResponseBuilder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void handleReturnValue(Object returnValue,
addHxHeaders(htmxResponse, webRequest.getNativeResponse(HttpServletResponse.class));
}

private View toView(HtmxResponse htmxResponse) {
View toView(HtmxResponse htmxResponse) {

Assert.notNull(htmxResponse, "HtmxResponse must not be null!");

Expand All @@ -74,7 +74,7 @@ private View toView(HtmxResponse htmxResponse) {
};
}

private void addHxHeaders(HtmxResponse htmxResponse, HttpServletResponse response) {
void addHxHeaders(HtmxResponse htmxResponse, HttpServletResponse response) {
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggersInternal());
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettleInternal());
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwapInternal());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ public void throwException() {
throw new RuntimeException("Fake exception");
}

@GetMapping("/argument")
public String argument(HtmxResponse.Builder htmxResponse) {
htmxResponse.trigger("event1");
return "argument";
}



@ExceptionHandler(Exception.class)
public HtmxResponse handleError(Exception ex) {
return HtmxResponse.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,11 @@ public void testException() throws Exception {
<span>Fake exception</span>
</span>""");
}

@Test
public void testHxTriggerArgument() throws Exception {
mockMvc.perform(get("/hvhi/argument"))
.andExpect(status().isOk())
.andExpect(header().string("HX-Trigger", "event1"));
}
}
Empty file.

0 comments on commit b117795

Please sign in to comment.