Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for processing HtmxResponse in the Model and as an Argument #128

Merged
merged 4 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
wimdeblauwe marked this conversation as resolved.
Show resolved Hide resolved

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.