Skip to content

Commit 2e08a6c

Browse files
authored
Merge pull request #152 from xhaggi/feature/gh-147
Upgrade to Spring Boot 3.4.0, remove deprecated code and mark HtmxView as deprecated
2 parents 0b4b4a9 + 7a47bb2 commit 2e08a6c

34 files changed

+382
-2796
lines changed

README.md

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,9 @@ public String users() {
175175
### HTML Fragments
176176

177177
In Spring MVC, view rendering typically involves specifying one view and one model. However, in htmx a common capability is to send multiple HTML fragments that
178-
htmx can use to update different parts of the page, which is called [Out Of Band Swaps](https://htmx.org/docs/#oob_swaps). For this, controller methods can return
179-
[HtmxView](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxView.html)
178+
htmx can use to update different parts of the page, which is called [Out Of Band Swaps](https://htmx.org/docs/#oob_swaps). Spring offers the ability to return
179+
multiple HTML fragments using `Collection<ModelAndView>` or `FragmentsRendering` as return type of controller. Further information on this can be found in the
180+
Spring Framework documentation under [HTML Fragments](https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-fragments.html).
180181

181182
```java
182183
@HxRequest
@@ -185,30 +186,13 @@ public View users(Model model) {
185186
model.addAttribute("users", userRepository.findAll());
186187
model.addAttribute("count", userRepository.count());
187188

188-
var view = new HtmxView();
189-
view.add("users/list");
190-
view.add("users/count");
191-
192-
return view;
189+
return FragmentsRendering
190+
.with("users/list")
191+
.fragment("users/count")
192+
.build();
193193
}
194194
```
195195

196-
An `HtmxView` can be formed from view names, as above, or fully resolved `View` instances, if the controller knows how
197-
to do that, or from `ModelAndView` instances (resolved or unresolved). Each fragment can have its own model, which is merged with the controller model before rendering.
198-
199-
```java
200-
@HxRequest
201-
@GetMapping("/users")
202-
public View users(Model model) {
203-
var view = new HtmxView();
204-
view.add("users/list", Map.of("users", userRepository.findAll()));
205-
view.add("users/count", Map.of("count", userRepository.count()));
206-
207-
return view;
208-
}
209-
```
210-
211-
212196
### Exceptions
213197

214198
It is also possible to use `HtmxRequest` and `HtmxResponse` as method argument in handler methods annotated with `@ExceptionHandler`.

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxExceptionHandlerExceptionResolver.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import jakarta.servlet.http.HttpServletRequest;
44
import jakarta.servlet.http.HttpServletResponse;
5+
import org.springframework.web.context.request.ServletWebRequest;
56
import org.springframework.web.method.HandlerMethod;
67
import org.springframework.web.servlet.ModelAndView;
78
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
@@ -26,7 +27,8 @@ public HtmxExceptionHandlerExceptionResolver(HtmxHandlerMethodAnnotationHandler
2627
@Override
2728
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {
2829

29-
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
30+
ServletWebRequest webRequest = new ServletWebRequest(request, response);
31+
ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception, webRequest);
3032
if (exceptionHandlerMethod != null) {
3133
Method method = exceptionHandlerMethod.getMethod();
3234
handlerMethodAnnotationHandler.handleMethod(method, request, response);

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,16 @@ public void afterCompletion(HttpServletRequest request, HttpServletResponse resp
3131

3232
HtmxResponse htmxResponse = RequestContextUtils.getHtmxResponse(request);
3333
if (htmxResponse != null) {
34-
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggersInternal());
35-
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettleInternal());
36-
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwapInternal());
37-
38-
if (htmxResponse.getLocation() != null) {
39-
HtmxLocation location = htmxResponse.getLocation();
40-
if (location.hasContextData()) {
41-
location.setPath(RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative()));
42-
setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION, location);
43-
} else {
44-
response.setHeader(HtmxResponseHeader.HX_LOCATION.getValue(), RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative()));
45-
}
46-
}
34+
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggers());
35+
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettle());
36+
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwap());
37+
4738
if (htmxResponse.getReplaceUrl() != null) {
4839
response.setHeader(HtmxResponseHeader.HX_REPLACE_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getReplaceUrl(), htmxResponse.isContextRelative()));
4940
}
5041
if (htmxResponse.getPushUrl() != null) {
5142
response.setHeader(HtmxResponseHeader.HX_PUSH_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getPushUrl(), htmxResponse.isContextRelative()));
5243
}
53-
if (htmxResponse.getRedirect() != null) {
54-
response.setHeader(HtmxResponseHeader.HX_REDIRECT.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getRedirect(), htmxResponse.isContextRelative()));
55-
}
56-
if (htmxResponse.isRefresh()) {
57-
response.setHeader(HtmxResponseHeader.HX_REFRESH.getValue(), "true");
58-
}
5944
if (htmxResponse.getRetarget() != null) {
6045
response.setHeader(HtmxResponseHeader.HX_RETARGET.getValue(), htmxResponse.getRetarget());
6146
}

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodAnnotationHandler.java

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@
55
import jakarta.servlet.http.HttpServletRequest;
66
import jakarta.servlet.http.HttpServletResponse;
77
import org.springframework.core.annotation.AnnotatedElementUtils;
8-
import org.springframework.http.HttpHeaders;
98

109
import java.lang.reflect.Method;
1110
import java.time.Duration;
1211

13-
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*;
14-
1512
/**
1613
* A handler for processing htmx annotations present on exception handler methods.
1714
*
@@ -56,56 +53,56 @@ private void setHxPushUrl(HttpServletRequest request, HttpServletResponse respon
5653
HxPushUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxPushUrl.class);
5754
if (methodAnnotation != null) {
5855
if (HtmxValue.TRUE.equals(methodAnnotation.value())) {
59-
setHeader(response, HX_PUSH_URL, getRequestUrl(request));
56+
setHeader(response, HtmxResponseHeader.HX_PUSH_URL, getRequestUrl(request));
6057
} else {
61-
setHeader(response, HX_PUSH_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
58+
setHeader(response, HtmxResponseHeader.HX_PUSH_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
6259
}
6360
}
6461
}
6562

6663
private void setHxRedirect(HttpServletRequest request, HttpServletResponse response, Method method) {
6764
HxRedirect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRedirect.class);
6865
if (methodAnnotation != null) {
69-
setHeader(response, HX_REDIRECT, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
66+
setHeader(response, HtmxResponseHeader.HX_REDIRECT, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
7067
}
7168
}
7269

7370
private void setHxReplaceUrl(HttpServletRequest request, HttpServletResponse response, Method method) {
7471
HxReplaceUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReplaceUrl.class);
7572
if (methodAnnotation != null) {
7673
if (HtmxValue.TRUE.equals(methodAnnotation.value())) {
77-
setHeader(response, HX_REPLACE_URL, getRequestUrl(request));
74+
setHeader(response, HtmxResponseHeader.HX_REPLACE_URL, getRequestUrl(request));
7875
} else {
79-
setHeader(response, HX_REPLACE_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
76+
setHeader(response, HtmxResponseHeader.HX_REPLACE_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
8077
}
8178
}
8279
}
8380

8481
private void setHxReswap(HttpServletResponse response, Method method) {
8582
HxReswap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReswap.class);
8683
if (methodAnnotation != null) {
87-
setHeader(response, HX_RESWAP, convertToReswap(methodAnnotation));
84+
setHeader(response, HtmxResponseHeader.HX_RESWAP, convertToReswap(methodAnnotation));
8885
}
8986
}
9087

9188
private void setHxRetarget(HttpServletResponse response, Method method) {
9289
HxRetarget methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRetarget.class);
9390
if (methodAnnotation != null) {
94-
setHeader(response, HX_RETARGET, methodAnnotation.value());
91+
setHeader(response, HtmxResponseHeader.HX_RETARGET, methodAnnotation.value());
9592
}
9693
}
9794

9895
private void setHxReselect(HttpServletResponse response, Method method) {
9996
HxReselect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReselect.class);
10097
if (methodAnnotation != null) {
101-
setHeader(response, HX_RESELECT, methodAnnotation.value());
98+
setHeader(response, HtmxResponseHeader.HX_RESELECT, methodAnnotation.value());
10299
}
103100
}
104101

105102
private void setHxTrigger(HttpServletResponse response, Method method) {
106103
HxTrigger methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTrigger.class);
107104
if (methodAnnotation != null) {
108-
setHeader(response, convertToHeader(methodAnnotation.lifecycle()), methodAnnotation.value());
105+
setHeader(response, HtmxResponseHeader.HX_TRIGGER, methodAnnotation.value());
109106
}
110107
}
111108

@@ -126,20 +123,7 @@ private void setHxTriggerAfterSwap(HttpServletResponse response, Method method)
126123
private void setHxRefresh(HttpServletResponse response, Method method) {
127124
HxRefresh methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRefresh.class);
128125
if (methodAnnotation != null) {
129-
setHeader(response, HX_REFRESH, HtmxValue.TRUE);
130-
}
131-
}
132-
133-
private HtmxResponseHeader convertToHeader(HxTriggerLifecycle lifecycle) {
134-
switch (lifecycle) {
135-
case RECEIVE:
136-
return HX_TRIGGER;
137-
case SETTLE:
138-
return HX_TRIGGER_AFTER_SETTLE;
139-
case SWAP:
140-
return HX_TRIGGER_AFTER_SWAP;
141-
default:
142-
throw new IllegalArgumentException("Unknown lifecycle:" + lifecycle);
126+
setHeader(response, HtmxResponseHeader.HX_REFRESH, HtmxValue.TRUE);
143127
}
144128
}
145129

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolver.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
1010
import org.springframework.web.method.support.ModelAndViewContainer;
1111

12-
import java.util.Objects;
13-
14-
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*;
15-
1612
public class HtmxHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
1713

1814
@Override

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,16 @@
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.fasterxml.jackson.databind.json.JsonMapper;
5-
import org.springframework.beans.factory.ObjectFactory;
6-
import org.springframework.beans.factory.annotation.Qualifier;
75
import org.springframework.boot.autoconfigure.AutoConfiguration;
86
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
97
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
108
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
119
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
1210
import org.springframework.context.annotation.Bean;
1311
import org.springframework.core.Ordered;
14-
import org.springframework.util.Assert;
1512
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
1613
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
17-
import org.springframework.web.servlet.LocaleResolver;
1814
import org.springframework.web.servlet.View;
19-
import org.springframework.web.servlet.ViewResolver;
2015
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
2116
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
2217
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
@@ -28,19 +23,10 @@
2823
@ConditionalOnWebApplication
2924
public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConfigurer {
3025

31-
private final ObjectFactory<ViewResolver> viewResolverObjectFactory;
32-
private final ObjectFactory<LocaleResolver> localeResolverObjectFactory;
3326
private final ObjectMapper objectMapper;
3427
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
3528

36-
HtmxMvcAutoConfiguration(@Qualifier("viewResolver") ObjectFactory<ViewResolver> viewResolverObjectFactory,
37-
ObjectFactory<LocaleResolver> localeResolverObjectFactory) {
38-
39-
Assert.notNull(viewResolverObjectFactory, "viewResolverObjectFactory must not be null!");
40-
Assert.notNull(localeResolverObjectFactory, "localeResolverObjectFactory must not be null!");
41-
42-
this.viewResolverObjectFactory = viewResolverObjectFactory;
43-
this.localeResolverObjectFactory = localeResolverObjectFactory;
29+
HtmxMvcAutoConfiguration() {
4430
this.objectMapper = JsonMapper.builder().build();
4531
this.handlerMethodAnnotationHandler = new HtmxHandlerMethodAnnotationHandler(this.objectMapper);
4632
}
@@ -63,8 +49,7 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
6349

6450
@Override
6551
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
66-
handlers.add(new HtmxResponseHandlerMethodReturnValueHandler(viewResolverObjectFactory.getObject(), localeResolverObjectFactory, objectMapper));
67-
handlers.add(new HtmxViewMethodReturnValueHandler(viewResolverObjectFactory.getObject(), localeResolverObjectFactory.getObject()));
52+
handlers.add(new HtmxViewMethodReturnValueHandler());
6853
}
6954

7055
@Override

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequest.java

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package io.github.wimdeblauwe.htmx.spring.boot.mvc;
22

3-
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*;
4-
53
import jakarta.servlet.http.HttpServletRequest;
64
import org.springframework.lang.Nullable;
75

6+
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*;
7+
88
/**
99
* This class can be used as a controller method argument to access
1010
* the <a href="https://htmx.org/reference/#request_headers">htmx Request Headers</a>.
@@ -188,98 +188,36 @@ public static final class Builder {
188188
private Builder() {
189189
}
190190

191-
/**
192-
* @deprecated use {@link #boosted(boolean)} instead. Will be removed in 4.0.
193-
*/
194-
@Deprecated
195-
public Builder withBoosted(boolean boosted) {
196-
return boosted(boosted);
197-
}
198-
199191
public Builder boosted(boolean boosted) {
200192
this.boosted = boosted;
201193
return this;
202194
}
203195

204-
/**
205-
* @deprecated use {@link #currentUrl(String)} instead. Will be removed in 4.0.
206-
*/
207-
@Deprecated
208-
public Builder withCurrentUrl(String currentUrl) {
209-
this.currentUrl = currentUrl;
210-
return this;
211-
}
212-
213196
public Builder currentUrl(String currentUrl) {
214197
this.currentUrl = currentUrl;
215198
return this;
216199
}
217200

218-
/**
219-
* @deprecated use {@link #historyRestoreRequest(boolean)} instead. Will be removed in 4.0.
220-
*/
221-
@Deprecated
222-
public Builder withHistoryRestoreRequest(boolean historyRestoreRequest) {
223-
this.historyRestoreRequest = historyRestoreRequest;
224-
return this;
225-
}
226-
227201
public Builder historyRestoreRequest(boolean historyRestoreRequest) {
228202
this.historyRestoreRequest = historyRestoreRequest;
229203
return this;
230204
}
231205

232-
/**
233-
* @deprecated use {@link #promptResponse(String)} instead. Will be removed in 4.0.
234-
*/
235-
@Deprecated
236-
public Builder withPromptResponse(String promptResponse) {
237-
this.promptResponse = promptResponse;
238-
return this;
239-
}
240-
241206
public Builder promptResponse(String promptResponse) {
242207
this.promptResponse = promptResponse;
243208
return this;
244209
}
245210

246-
/**
247-
* @deprecated use {@link #target(String)} instead. Will be removed in 4.0.
248-
*/
249-
@Deprecated
250-
public Builder withTarget(String target) {
251-
this.target = target;
252-
return this;
253-
}
254-
255211
public Builder target(String target) {
256212
this.target = target;
257213
return this;
258214
}
259215

260-
/**
261-
* @deprecated use {@link #triggerName(String)} instead. Will be removed in 4.0.
262-
*/
263-
@Deprecated
264-
public Builder withTriggerName(String triggerName) {
265-
this.triggerName = triggerName;
266-
return this;
267-
}
268-
269216
public Builder triggerName(String triggerName) {
270217
this.triggerName = triggerName;
271218
return this;
272219
}
273220

274-
/**
275-
* @deprecated use {@link #triggerId(String)} instead. Will be removed in 4.0.
276-
*/
277-
@Deprecated
278-
public Builder withTriggerId(String triggerId) {
279-
this.triggerId = triggerId;
280-
return this;
281-
}
282-
283221
public Builder triggerId(String triggerId) {
284222
this.triggerId = triggerId;
285223
return this;

0 commit comments

Comments
 (0)