Skip to content

Commit

Permalink
Mark HtmxView as deprecated in favor of HTML Fragments support in Spr…
Browse files Browse the repository at this point in the history
  • Loading branch information
xhaggi committed Nov 25, 2024
1 parent e9c2d78 commit 7a47bb2
Show file tree
Hide file tree
Showing 5 changed files with 16 additions and 101 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,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,7 +49,7 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)

@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
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
Expand Up @@ -2,9 +2,7 @@

import org.springframework.util.Assert;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.*;

/**
* A holder for htmx-related response headers that can be used as method argument in controllers.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
* this class.
*
* @since 3.6.0
* @deprecated since 4.0.0 for removal in 4.1.0 in favor of <a href="https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-fragments.html">HTML Fragments</a> support.
*/
@Deprecated
public class HtmxView {

private final Set<ModelAndView> views = new LinkedHashSet<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,20 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
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.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.util.Collection;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.servlet.view.FragmentsRendering;

/**
* Handles return values that are of type {@link HtmxView}.
*
* @since 3.6.0
* @deprecated since 4.0.0 for removal in 4.1.0 in favor of <a href="https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-fragments.html">HTML Fragments</a> support.
*/
@Deprecated
public class HtmxViewMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

private final ViewResolver viewResolver;
private final LocaleResolver localeResolver;

public HtmxViewMethodReturnValueHandler(ViewResolver viewResolver, LocaleResolver localeResolver) {
this.viewResolver = viewResolver;
this.localeResolver = localeResolver;
}

@Override
public boolean supportsReturnType(MethodParameter returnType) {
return HtmxView.class.isAssignableFrom(returnType.getParameterType());
Expand All @@ -52,40 +27,10 @@ public void handleReturnValue(Object returnValue,
NativeWebRequest webRequest) throws Exception {

if (returnValue instanceof HtmxView htmxView) {
mavContainer.setView(toView(htmxView));
mavContainer.setView(FragmentsRendering.with(htmxView.getViews()).build());
} else if (returnValue != null) {
throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}

private View toView(HtmxView htmxView) {

return (model, request, response) -> {
Locale locale = localeResolver.resolveLocale(request);
ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response);

for (ModelAndView mav : htmxView.getViews()) {
View view;
if (mav.isReference()) {
view = viewResolver.resolveViewName(mav.getViewName(), locale);
if (view == null) {
throw new IllegalArgumentException("Could not resolve view with name '" + mav.getViewName() + "'.");
}
} else {
view = mav.getView();
}

for (String key : model.keySet()) {
if (!mav.getModel().containsKey(key)) {
mav.getModel().put(key, model.get(key));
}
}

view.render(mav.getModel(), request, wrapper);
}

wrapper.copyBodyToResponse();
};
}

}

0 comments on commit 7a47bb2

Please sign in to comment.