Skip to content

Commit

Permalink
Merge pull request #152 from xhaggi/feature/gh-147
Browse files Browse the repository at this point in the history
Upgrade to Spring Boot 3.4.0, remove deprecated code and mark HtmxView as deprecated
  • Loading branch information
xhaggi authored Nov 25, 2024
2 parents 0b4b4a9 + 7a47bb2 commit 2e08a6c
Show file tree
Hide file tree
Showing 34 changed files with 382 additions and 2,796 deletions.
30 changes: 7 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ public String users() {
### HTML Fragments

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
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
[HtmxView](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxView.html)
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
multiple HTML fragments using `Collection<ModelAndView>` or `FragmentsRendering` as return type of controller. Further information on this can be found in the
Spring Framework documentation under [HTML Fragments](https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-fragments.html).

```java
@HxRequest
Expand All @@ -185,30 +186,13 @@ public View users(Model model) {
model.addAttribute("users", userRepository.findAll());
model.addAttribute("count", userRepository.count());

var view = new HtmxView();
view.add("users/list");
view.add("users/count");

return view;
return FragmentsRendering
.with("users/list")
.fragment("users/count")
.build();
}
```

An `HtmxView` can be formed from view names, as above, or fully resolved `View` instances, if the controller knows how
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.

```java
@HxRequest
@GetMapping("/users")
public View users(Model model) {
var view = new HtmxView();
view.add("users/list", Map.of("users", userRepository.findAll()));
view.add("users/count", Map.of("count", userRepository.count()));

return view;
}
```


### Exceptions

It is also possible to use `HtmxRequest` and `HtmxResponse` as method argument in handler methods annotated with `@ExceptionHandler`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
Expand All @@ -26,7 +27,8 @@ public HtmxExceptionHandlerExceptionResolver(HtmxHandlerMethodAnnotationHandler
@Override
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {

ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception, webRequest);
if (exceptionHandlerMethod != null) {
Method method = exceptionHandlerMethod.getMethod();
handlerMethodAnnotationHandler.handleMethod(method, request, response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,16 @@ public void afterCompletion(HttpServletRequest request, HttpServletResponse resp

HtmxResponse htmxResponse = RequestContextUtils.getHtmxResponse(request);
if (htmxResponse != null) {
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) {
HtmxLocation location = htmxResponse.getLocation();
if (location.hasContextData()) {
location.setPath(RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative()));
setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION, location);
} else {
response.setHeader(HtmxResponseHeader.HX_LOCATION.getValue(), RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative()));
}
}
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggers());
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettle());
addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwap());

if (htmxResponse.getReplaceUrl() != null) {
response.setHeader(HtmxResponseHeader.HX_REPLACE_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getReplaceUrl(), htmxResponse.isContextRelative()));
}
if (htmxResponse.getPushUrl() != null) {
response.setHeader(HtmxResponseHeader.HX_PUSH_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getPushUrl(), htmxResponse.isContextRelative()));
}
if (htmxResponse.getRedirect() != null) {
response.setHeader(HtmxResponseHeader.HX_REDIRECT.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getRedirect(), htmxResponse.isContextRelative()));
}
if (htmxResponse.isRefresh()) {
response.setHeader(HtmxResponseHeader.HX_REFRESH.getValue(), "true");
}
if (htmxResponse.getRetarget() != null) {
response.setHeader(HtmxResponseHeader.HX_RETARGET.getValue(), htmxResponse.getRetarget());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpHeaders;

import java.lang.reflect.Method;
import java.time.Duration;

import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*;

/**
* A handler for processing htmx annotations present on exception handler methods.
*
Expand Down Expand Up @@ -56,56 +53,56 @@ private void setHxPushUrl(HttpServletRequest request, HttpServletResponse respon
HxPushUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxPushUrl.class);
if (methodAnnotation != null) {
if (HtmxValue.TRUE.equals(methodAnnotation.value())) {
setHeader(response, HX_PUSH_URL, getRequestUrl(request));
setHeader(response, HtmxResponseHeader.HX_PUSH_URL, getRequestUrl(request));
} else {
setHeader(response, HX_PUSH_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
setHeader(response, HtmxResponseHeader.HX_PUSH_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
}
}
}

private void setHxRedirect(HttpServletRequest request, HttpServletResponse response, Method method) {
HxRedirect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRedirect.class);
if (methodAnnotation != null) {
setHeader(response, HX_REDIRECT, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
setHeader(response, HtmxResponseHeader.HX_REDIRECT, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
}
}

private void setHxReplaceUrl(HttpServletRequest request, HttpServletResponse response, Method method) {
HxReplaceUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReplaceUrl.class);
if (methodAnnotation != null) {
if (HtmxValue.TRUE.equals(methodAnnotation.value())) {
setHeader(response, HX_REPLACE_URL, getRequestUrl(request));
setHeader(response, HtmxResponseHeader.HX_REPLACE_URL, getRequestUrl(request));
} else {
setHeader(response, HX_REPLACE_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
setHeader(response, HtmxResponseHeader.HX_REPLACE_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
}
}
}

private void setHxReswap(HttpServletResponse response, Method method) {
HxReswap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReswap.class);
if (methodAnnotation != null) {
setHeader(response, HX_RESWAP, convertToReswap(methodAnnotation));
setHeader(response, HtmxResponseHeader.HX_RESWAP, convertToReswap(methodAnnotation));
}
}

private void setHxRetarget(HttpServletResponse response, Method method) {
HxRetarget methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRetarget.class);
if (methodAnnotation != null) {
setHeader(response, HX_RETARGET, methodAnnotation.value());
setHeader(response, HtmxResponseHeader.HX_RETARGET, methodAnnotation.value());
}
}

private void setHxReselect(HttpServletResponse response, Method method) {
HxReselect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReselect.class);
if (methodAnnotation != null) {
setHeader(response, HX_RESELECT, methodAnnotation.value());
setHeader(response, HtmxResponseHeader.HX_RESELECT, methodAnnotation.value());
}
}

private void setHxTrigger(HttpServletResponse response, Method method) {
HxTrigger methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTrigger.class);
if (methodAnnotation != null) {
setHeader(response, convertToHeader(methodAnnotation.lifecycle()), methodAnnotation.value());
setHeader(response, HtmxResponseHeader.HX_TRIGGER, methodAnnotation.value());
}
}

Expand All @@ -126,20 +123,7 @@ private void setHxTriggerAfterSwap(HttpServletResponse response, Method method)
private void setHxRefresh(HttpServletResponse response, Method method) {
HxRefresh methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRefresh.class);
if (methodAnnotation != null) {
setHeader(response, HX_REFRESH, HtmxValue.TRUE);
}
}

private HtmxResponseHeader convertToHeader(HxTriggerLifecycle lifecycle) {
switch (lifecycle) {
case RECEIVE:
return HX_TRIGGER;
case SETTLE:
return HX_TRIGGER_AFTER_SETTLE;
case SWAP:
return HX_TRIGGER_AFTER_SWAP;
default:
throw new IllegalArgumentException("Unknown lifecycle:" + lifecycle);
setHeader(response, HtmxResponseHeader.HX_REFRESH, HtmxValue.TRUE);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import java.util.Objects;

import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*;

public class HtmxHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.util.Assert;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
Expand All @@ -28,19 +23,10 @@
@ConditionalOnWebApplication
public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConfigurer {

private final ObjectFactory<ViewResolver> viewResolverObjectFactory;
private final ObjectFactory<LocaleResolver> localeResolverObjectFactory;
private final ObjectMapper objectMapper;
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;

HtmxMvcAutoConfiguration(@Qualifier("viewResolver") ObjectFactory<ViewResolver> viewResolverObjectFactory,
ObjectFactory<LocaleResolver> localeResolverObjectFactory) {

Assert.notNull(viewResolverObjectFactory, "viewResolverObjectFactory must not be null!");
Assert.notNull(localeResolverObjectFactory, "localeResolverObjectFactory must not be null!");

this.viewResolverObjectFactory = viewResolverObjectFactory;
this.localeResolverObjectFactory = localeResolverObjectFactory;
HtmxMvcAutoConfiguration() {
this.objectMapper = JsonMapper.builder().build();
this.handlerMethodAnnotationHandler = new HtmxHandlerMethodAnnotationHandler(this.objectMapper);
}
Expand All @@ -63,8 +49,7 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)

@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
handlers.add(new HtmxResponseHandlerMethodReturnValueHandler(viewResolverObjectFactory.getObject(), localeResolverObjectFactory, objectMapper));
handlers.add(new HtmxViewMethodReturnValueHandler(viewResolverObjectFactory.getObject(), localeResolverObjectFactory.getObject()));
handlers.add(new HtmxViewMethodReturnValueHandler());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.lang.Nullable;

import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*;

/**
* This class can be used as a controller method argument to access
* the <a href="https://htmx.org/reference/#request_headers">htmx Request Headers</a>.
Expand Down Expand Up @@ -188,98 +188,36 @@ public static final class Builder {
private Builder() {
}

/**
* @deprecated use {@link #boosted(boolean)} instead. Will be removed in 4.0.
*/
@Deprecated
public Builder withBoosted(boolean boosted) {
return boosted(boosted);
}

public Builder boosted(boolean boosted) {
this.boosted = boosted;
return this;
}

/**
* @deprecated use {@link #currentUrl(String)} instead. Will be removed in 4.0.
*/
@Deprecated
public Builder withCurrentUrl(String currentUrl) {
this.currentUrl = currentUrl;
return this;
}

public Builder currentUrl(String currentUrl) {
this.currentUrl = currentUrl;
return this;
}

/**
* @deprecated use {@link #historyRestoreRequest(boolean)} instead. Will be removed in 4.0.
*/
@Deprecated
public Builder withHistoryRestoreRequest(boolean historyRestoreRequest) {
this.historyRestoreRequest = historyRestoreRequest;
return this;
}

public Builder historyRestoreRequest(boolean historyRestoreRequest) {
this.historyRestoreRequest = historyRestoreRequest;
return this;
}

/**
* @deprecated use {@link #promptResponse(String)} instead. Will be removed in 4.0.
*/
@Deprecated
public Builder withPromptResponse(String promptResponse) {
this.promptResponse = promptResponse;
return this;
}

public Builder promptResponse(String promptResponse) {
this.promptResponse = promptResponse;
return this;
}

/**
* @deprecated use {@link #target(String)} instead. Will be removed in 4.0.
*/
@Deprecated
public Builder withTarget(String target) {
this.target = target;
return this;
}

public Builder target(String target) {
this.target = target;
return this;
}

/**
* @deprecated use {@link #triggerName(String)} instead. Will be removed in 4.0.
*/
@Deprecated
public Builder withTriggerName(String triggerName) {
this.triggerName = triggerName;
return this;
}

public Builder triggerName(String triggerName) {
this.triggerName = triggerName;
return this;
}

/**
* @deprecated use {@link #triggerId(String)} instead. Will be removed in 4.0.
*/
@Deprecated
public Builder withTriggerId(String triggerId) {
this.triggerId = triggerId;
return this;
}

public Builder triggerId(String triggerId) {
this.triggerId = triggerId;
return this;
Expand Down
Loading

0 comments on commit 2e08a6c

Please sign in to comment.