-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow HtmxResponse to be used as return type in error handlers
Fixes #94
- Loading branch information
1 parent
93ed4cd
commit 1f29cd7
Showing
8 changed files
with
233 additions
and
127 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
.../github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package io.github.wimdeblauwe.htmx.spring.boot.mvc; | ||
|
||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.core.MethodParameter; | ||
import org.springframework.web.context.request.NativeWebRequest; | ||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler; | ||
import org.springframework.web.method.support.ModelAndViewContainer; | ||
|
||
public class HtmxResponseHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler { | ||
private final HtmxResponseHelper responseHelper; | ||
|
||
public HtmxResponseHandlerMethodReturnValueHandler(HtmxResponseHelper responseHelper) { | ||
this.responseHelper = responseHelper; | ||
} | ||
|
||
@Override | ||
public boolean supportsReturnType(MethodParameter returnType) { | ||
return returnType.getParameterType().equals(HtmxResponse.class); | ||
} | ||
|
||
@Override | ||
public void handleReturnValue(Object returnValue, | ||
MethodParameter returnType, | ||
ModelAndViewContainer mavContainer, | ||
NativeWebRequest webRequest) throws Exception { | ||
|
||
HtmxResponse htmxResponse = (HtmxResponse) returnValue; | ||
mavContainer.setView(responseHelper.toView(htmxResponse)); | ||
|
||
responseHelper.addHxHeaders(htmxResponse, webRequest.getNativeResponse(HttpServletResponse.class)); | ||
} | ||
} |
121 changes: 121 additions & 0 deletions
121
...ing-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package io.github.wimdeblauwe.htmx.spring.boot.mvc; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.beans.factory.ObjectFactory; | ||
import org.springframework.util.Assert; | ||
import org.springframework.web.servlet.LocaleResolver; | ||
import org.springframework.web.servlet.ModelAndView; | ||
import org.springframework.web.servlet.View; | ||
import org.springframework.web.servlet.ViewResolver; | ||
import org.springframework.web.util.ContentCachingResponseWrapper; | ||
|
||
import java.util.Collection; | ||
import java.util.HashMap; | ||
import java.util.Locale; | ||
import java.util.stream.Collectors; | ||
|
||
public class HtmxResponseHelper { | ||
private final ViewResolver views; | ||
private final ObjectFactory<LocaleResolver> locales; | ||
private final ObjectMapper objectMapper; | ||
|
||
public HtmxResponseHelper(ViewResolver views, | ||
ObjectFactory<LocaleResolver> locales, | ||
ObjectMapper objectMapper) { | ||
this.views = views; | ||
this.locales = locales; | ||
this.objectMapper = objectMapper; | ||
} | ||
|
||
public View toView(HtmxResponse htmxResponse) { | ||
|
||
Assert.notNull(htmxResponse, "HtmxResponse must not be null!"); | ||
|
||
return (model, request, response) -> { | ||
Locale locale = locales.getObject().resolveLocale(request); | ||
ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response); | ||
for (ModelAndView modelAndView : htmxResponse.getViews()) { | ||
View view = modelAndView.getView(); | ||
if (view == null) { | ||
view = views.resolveViewName(modelAndView.getViewName(), locale); | ||
} | ||
for (String key : model.keySet()) { | ||
if (!modelAndView.getModel().containsKey(key)) { | ||
modelAndView.getModel().put(key, model.get(key)); | ||
} | ||
} | ||
Assert.notNull(view, "Template '" + modelAndView + "' could not be resolved"); | ||
view.render(modelAndView.getModel(), request, wrapper); | ||
} | ||
wrapper.copyBodyToResponse(); | ||
}; | ||
} | ||
|
||
public 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()); | ||
|
||
if (htmxResponse.getLocation() != null) { | ||
if (htmxResponse.getLocation().hasContextData()) { | ||
setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION.getValue(), htmxResponse.getLocation()); | ||
} else { | ||
response.setHeader(HtmxResponseHeader.HX_LOCATION.getValue(), htmxResponse.getLocation().getPath()); | ||
} | ||
} | ||
if (htmxResponse.getReplaceUrl() != null) { | ||
response.setHeader(HtmxResponseHeader.HX_REPLACE_URL.getValue(), htmxResponse.getReplaceUrl()); | ||
} | ||
if (htmxResponse.getPushUrl() != null) { | ||
response.setHeader(HtmxResponseHeader.HX_PUSH_URL.getValue(), htmxResponse.getPushUrl()); | ||
} | ||
if (htmxResponse.getRedirect() != null) { | ||
response.setHeader(HtmxResponseHeader.HX_REDIRECT.getValue(), htmxResponse.getRedirect()); | ||
} | ||
if (htmxResponse.isRefresh()) { | ||
response.setHeader(HtmxResponseHeader.HX_REFRESH.getValue(), "true"); | ||
} | ||
if (htmxResponse.getRetarget() != null) { | ||
response.setHeader(HtmxResponseHeader.HX_RETARGET.getValue(), htmxResponse.getRetarget()); | ||
} | ||
if (htmxResponse.getReselect() != null) { | ||
response.setHeader(HtmxResponseHeader.HX_RESELECT.getValue(), htmxResponse.getReselect()); | ||
} | ||
if (htmxResponse.getReswap() != null) { | ||
response.setHeader(HtmxResponseHeader.HX_RESWAP.getValue(), htmxResponse.getReswap().toHeaderValue()); | ||
} | ||
} | ||
|
||
private void addHxTriggerHeaders(HttpServletResponse response, HtmxResponseHeader headerName, Collection<HtmxTrigger> triggers) { | ||
if (triggers.isEmpty()) { | ||
return; | ||
} | ||
|
||
// separate event names by commas if no additional details are available | ||
if (triggers.stream().allMatch(t -> t.getEventDetail() == null)) { | ||
String value = triggers.stream() | ||
.map(HtmxTrigger::getEventName) | ||
.collect(Collectors.joining(",")); | ||
|
||
response.setHeader(headerName.getValue(), value); | ||
return; | ||
} | ||
|
||
// multiple events with or without details | ||
var triggerMap = new HashMap<String, Object>(); | ||
for (HtmxTrigger trigger : triggers) { | ||
triggerMap.put(trigger.getEventName(), trigger.getEventDetail()); | ||
} | ||
setHeaderJsonValue(response, headerName.getValue(), triggerMap); | ||
} | ||
|
||
private void setHeaderJsonValue(HttpServletResponse response, String name, Object value) { | ||
try { | ||
response.setHeader(name, objectMapper.writeValueAsString(value)); | ||
} catch (JsonProcessingException e) { | ||
throw new IllegalArgumentException("Unable to set header " + name + " to " + value, e); | ||
} | ||
} | ||
} |
Oops, something went wrong.