diff --git a/framework-docs/modules/ROOT/pages/web/webflux-view.adoc b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc index 2236ce36182..305d518d472 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-view.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc @@ -448,7 +448,7 @@ Java:: ---- @GetMapping FragmentsRendering handle() { - return FragmentsRendering.with("posts").fragment("comments").build(); + return FragmentsRendering.fragment("posts").fragment("comments").build(); } ---- @@ -458,7 +458,7 @@ Kotlin:: ---- @GetMapping fun handle(): FragmentsRendering { - return FragmentsRendering.with("posts").fragment("comments").build() + return FragmentsRendering.fragment("posts").fragment("comments").build() } ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-fragments.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-fragments.adoc index 2e14cf0d056..10a4842bf0a 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-fragments.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-fragments.adoc @@ -48,7 +48,7 @@ Java:: ---- @GetMapping FragmentsRendering handle() { - return FragmentsRendering.with("posts").fragment("comments").build(); + return FragmentsRendering.fragment("posts").fragment("comments").build(); } ---- @@ -58,7 +58,7 @@ Kotlin:: ---- @GetMapping fun handle(): FragmentsRendering { - return FragmentsRendering.with("posts").fragment("comments").build() + return FragmentsRendering.fragment("posts").fragment("comments").build() } ---- ====== diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/DefaultFragmentsRenderingBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/DefaultFragmentsRenderingBuilder.java index a73da7e4938..1ced51d1cf1 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/DefaultFragmentsRenderingBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/DefaultFragmentsRenderingBuilder.java @@ -49,8 +49,8 @@ class DefaultFragmentsRenderingBuilder implements FragmentsRendering.Builder { @Nullable private HttpHeaders headers; - DefaultFragmentsRenderingBuilder(Collection fragments) { - this.fragmentsCollection = new ArrayList<>(fragments); + DefaultFragmentsRenderingBuilder() { + this.fragmentsCollection = null; this.fragmentsFlux = null; } @@ -85,13 +85,13 @@ class DefaultFragmentsRenderingBuilder implements FragmentsRendering.Builder { } @Override - public FragmentsRendering.Builder fragment(String viewName, Map model) { - return fragment(Fragment.create(viewName, model)); + public FragmentsRendering.Builder fragment(String viewName) { + return fragment(Fragment.create(viewName)); } @Override - public FragmentsRendering.Builder fragment(String viewName) { - return fragment(Fragment.create(viewName)); + public FragmentsRendering.Builder fragment(String viewName, Map model) { + return fragment(Fragment.create(viewName, model)); } @Override @@ -100,6 +100,12 @@ class DefaultFragmentsRenderingBuilder implements FragmentsRendering.Builder { return this; } + @Override + public FragmentsRendering.Builder fragments(Collection fragments) { + initFragmentsCollection().addAll(fragments); + return this; + } + private Collection initFragmentsCollection() { if (this.fragmentsCollection == null) { this.fragmentsCollection = new ArrayList<>(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/FragmentsRendering.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/FragmentsRendering.java index d30b510e99b..b454d09f9d0 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/FragmentsRendering.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/FragmentsRendering.java @@ -17,7 +17,6 @@ package org.springframework.web.reactive.result.view; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -32,13 +31,16 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Public API for HTML rendering of a collection of fragments each with a view - * and independent model. For use with frontends technologies such as + * Public API to render HTML fragments. A fragment is a portion of an HTML page. + * Normally HTML is rendered with a single model and view. This API allows + * using multiple model and view pairs, one for each HTML fragment. + * + *

For use with frontends technologies such as * htmx where multiple page fragments may be - * rendered in one response. Supported as a return value from Spring WebFlux - * controller methods. + * rendered in one response. * - *

For full page rendering with a single model and view, use {@link Rendering}. + *

Supported as a return value from annotated controller methods. + * For full page rendering with a single model and view, use {@link Rendering}. * * @author Rossen Stoyanchev * @since 6.2 @@ -63,59 +65,107 @@ public interface FragmentsRendering { /** - * Create a builder and add a fragment with a view name and a model. + * Create a builder with one HTML fragment, also inheriting attributes from + * the shared model for the request. * @param viewName the name of the view for the fragment - * @param model attributes for the fragment in addition to model - * attributes inherited from the model for the request * @return this builder + * @since 6.2.13 */ - static Builder with(String viewName, Map model) { - return withCollection(List.of(Fragment.create(viewName, model))); + static Builder fragment(String viewName) { + return new DefaultFragmentsRenderingBuilder().fragment(viewName); } /** - * Variant of {@link #with(String, Map)} with a view name only, but also - * inheriting model attributes from the shared model for the request. - * @param viewName the name of the view for the fragment + * Create a builder with one HTML fragment. + * @param viewName the view name for the fragment + * @param model attributes for the fragment, in addition to attributes from the + * shared model for the request * @return this builder + * @since 6.2.13 */ - static Builder with(String viewName) { - return withCollection(List.of(Fragment.create(viewName))); + static Builder fragment(String viewName, Map model) { + return new DefaultFragmentsRenderingBuilder().fragment(viewName, model); } /** - * Variant of {@link #with(String, Map)} with a collection of fragments. - * @param fragments the fragments to add; each fragment also inherits model + * Create a builder with multiple HTML fragments. + * @param fragments the fragments to add; each fragment also inherits * attributes from the shared model for the request * @return the created builder + * @since 6.2.13 */ - static Builder withCollection(Collection fragments) { - return new DefaultFragmentsRenderingBuilder(fragments); + static Builder fragments(Collection fragments) { + return new DefaultFragmentsRenderingBuilder().fragments(fragments); } /** - * Variant of {@link #with(String, Map)} with a {@link Publisher} of fragments. + * Create a builder with a {@link Publisher} of fragments. * @param fragmentsPublisher the fragments to add; each fragment also * inherits model attributes from the shared model for the request * @return the created builder + * @since 6.2.13 */ - static

> Builder withPublisher(P fragmentsPublisher) { + static

> Builder fragmentsPublisher(P fragmentsPublisher) { return new DefaultFragmentsRenderingBuilder(fragmentsPublisher); } /** - * Variant of {@link #withPublisher(Publisher)} that allows using any + * Variant of {@link #fragmentsPublisher(Publisher)} that allows using any * producer that can be resolved to {@link Publisher} via * {@link ReactiveAdapterRegistry}. + * @since 6.2.13 */ - static Builder withProducer(Object fragmentsProducer) { - return new DefaultFragmentsRenderingBuilder(adaptProducer(fragmentsProducer)); + static Builder fragmentsProducer(Object fragmentsProducer) { + ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(fragmentsProducer.getClass()); + Assert.isTrue(adapter != null, "Unknown producer " + fragmentsProducer.getClass()); + Publisher publisher = adapter.toPublisher(fragmentsProducer); + return fragmentsPublisher(publisher); } - private static Publisher adaptProducer(Object producer) { - ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(producer.getClass()); - Assert.isTrue(adapter != null, "Unknown producer " + producer.getClass()); - return adapter.toPublisher(producer); + + /** + * The same as {@link #fragment(String, Map)}. + * @deprecated in favor of {@link #fragment(String, Map)} + */ + @Deprecated(since = "6.2.13", forRemoval = true) + static Builder with(String viewName, Map model) { + return fragment(viewName, model); + } + + /** + * The same as {@link #fragments(Collection)}. + * @deprecated in favor of {@link #fragments(Collection)} + */ + @Deprecated(since = "6.2.13", forRemoval = true) + static Builder with(String viewName) { + return fragment(viewName); + } + + /** + * The same as {@link #fragments(Collection)}. + * @deprecated in favor of {@link #fragments(Collection)} + */ + @Deprecated(since = "6.2.13", forRemoval = true) + static Builder withCollection(Collection fragments) { + return fragments(fragments); + } + + /** + * The same as {@link #fragmentsPublisher(Publisher)}. + * @deprecated in favor of {@link #fragmentsPublisher(Publisher)} + */ + @Deprecated(since = "6.2.13", forRemoval = true) + static

> Builder withPublisher(P fragmentsPublisher) { + return fragmentsPublisher(fragmentsPublisher); + } + + /** + * The same as {@link #fragmentsProducer(Object)}. + * @deprecated in favor of {@link #fragmentsProducer(Object)} + */ + @Deprecated(since = "6.2.13", forRemoval = true) + static Builder withProducer(Object fragmentsProducer) { + return fragmentsProducer(fragmentsProducer); } @@ -148,30 +198,39 @@ public interface FragmentsRendering { Builder headers(Consumer headersConsumer); /** - * Add a fragment with a view name and a model. - * @param viewName the name of the view for the fragment - * @param model attributes for the fragment in addition to model - * attributes inherited from the model for the request + * Add an HTML fragment. + * @param viewName the view name for the fragment + * @param model fragment attributes in addition to attributes from the + * shared model for the request * @return this builder */ Builder fragment(String viewName, Map model); /** - * Variant of {@link #fragment(String, Map)} with a view name only, where - * the fragment model also inherits model attributes from the shared + * Add an HTML fragment. The fragment will use attributes from the shared * model for the request. - * @param viewName the name of the view for the fragment + * @param viewName the view name for the fragment * @return this builder */ Builder fragment(String viewName); /** - * Variant of {@link #fragment(String, Map)} with a {@link Fragment}. - * @param fragment the fragment to add + * Add an HTML fragment. + * @param fragment the fragment to add; the fragment also inherits + * attributes from the shared model for the request * @return this builder */ Builder fragment(Fragment fragment); + /** + * Add HTML fragments. + * @param fragments the fragments to add; each fragment also inherits + * attributes from the shared model for the request + * @return this builder + * @since 6.2.13 + */ + Builder fragments(Collection fragments); + /** * Build the {@link FragmentsRendering} instance. */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 5380afde7fd..bfc24cb9abe 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -220,7 +220,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp } valueMono = (result.getReturnValue() != null ? - Mono.just(FragmentsRendering.withPublisher(adapter.toPublisher(result.getReturnValue())).build()) : + Mono.just(FragmentsRendering.fragmentsPublisher(adapter.toPublisher(result.getReturnValue())).build()) : Mono.empty()); valueType = ResolvableType.forClass(FragmentsRendering.class); @@ -252,7 +252,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp } if (Collection.class.isAssignableFrom(clazz)) { - returnValue = FragmentsRendering.withCollection((Collection) returnValue).build(); + returnValue = FragmentsRendering.fragments((Collection) returnValue).build(); clazz = FragmentsRendering.class; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java index 0973e1d27be..8bf3d78db5b 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java @@ -66,9 +66,9 @@ public class FragmentViewResolutionResultHandlerTests { static Stream arguments() { Flux fragmentFlux = Flux.just(fragment1, fragment2).subscribeOn(Schedulers.boundedElastic()); return Stream.of( - Arguments.of(FragmentsRendering.withPublisher(fragmentFlux).build(), + Arguments.of(FragmentsRendering.fragmentsPublisher(fragmentFlux).build(), on(Handler.class).resolveReturnType(FragmentsRendering.class)), - Arguments.of(FragmentsRendering.withCollection(List.of(fragment1, fragment2)).build(), + Arguments.of(FragmentsRendering.fragments(List.of(fragment1, fragment2)).build(), on(Handler.class).resolveReturnType(FragmentsRendering.class)), Arguments.of(fragmentFlux, on(Handler.class).resolveReturnType(Flux.class, Fragment.class)), diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java index e4e57938e88..c09690f1744 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java @@ -92,7 +92,7 @@ public class ModelAndViewMethodReturnValueHandler implements HandlerMethodReturn } if (returnValue instanceof Collection mavs) { - returnValue = FragmentsRendering.with((Collection) mavs).build(); + returnValue = FragmentsRendering.fragments((Collection) mavs).build(); } if (returnValue instanceof FragmentsRendering rendering) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java index 73992490bb7..f293316d4f3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java @@ -398,7 +398,7 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur FragmentHttpServletResponse fragmentResponse = new FragmentHttpServletResponse(this.response, this.charset); - FragmentsRendering render = FragmentsRendering.with(List.of(modelAndView)).build(); + FragmentsRendering render = FragmentsRendering.fragments(List.of(modelAndView)).build(); render.resolveNestedViews(this::resolveViewName, this.locale); render.render(modelAndView.getModel(), this.request, fragmentResponse); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/FragmentsRendering.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/FragmentsRendering.java index 54a2f6ae1b2..edf33e7e5f8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/FragmentsRendering.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/FragmentsRendering.java @@ -27,11 +27,15 @@ import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.SmartView; /** - * Public API for HTML rendering of a collection of fragments each with a view - * and independent model. For use with frontends technologies such as + * Public API to render HTML fragments. A fragment is a portion of an HTML page. + * Normally HTML is rendered with a single model and view. This API allows + * using multiple model and view pairs, one for each HTML fragment. + * + *

For use with frontends technologies such as * htmx where multiple page fragments may be - * rendered in one response. Supported as a return value from Spring MVC - * controller methods. + * rendered in one response. + * + *

Supported as a return value from controller methods. * * @author Rossen Stoyanchev * @since 6.2 @@ -51,37 +55,68 @@ public interface FragmentsRendering extends SmartView { /** - * Create a builder and add a fragment with a view name and a model. + * Create a builder with one HTML fragment, also inheriting attributes from + * the shared model for the request. * @param viewName the name of the view for the fragment - * @param model attributes for the fragment in addition to model - * attributes inherited from the shared model for the request * @return the created builder + * @since 6.2.13 */ - static Builder with(String viewName, Map model) { - return new DefaultFragmentsRenderingBuilder().fragment(viewName, model); + static Builder fragment(String viewName) { + return new DefaultFragmentsRenderingBuilder().fragment(viewName); } /** - * Variant of {@link #with(String, Map)} with a view name only, but also - * inheriting model attributes from the shared model for the request. - * @param viewName the name of the view for the fragment + * Create a builder with one HTML fragment. + * @param viewName the view name for the fragment + * @param model attributes for the fragment, in addition to attributes from the + * shared model for the request * @return the created builder + * @since 6.2.13 */ - static Builder with(String viewName) { - return new DefaultFragmentsRenderingBuilder().fragment(viewName); + static Builder fragment(String viewName, Map model) { + return new DefaultFragmentsRenderingBuilder().fragment(viewName, model); } /** - * Variant of {@link #with(String, Map)} with a collection of fragments. - * @param fragments the fragments to add; each fragment also inherits model + * Create a builder with multiple HTML fragments. + * @param fragments the fragments to add; each fragment also inherits * attributes from the shared model for the request * @return the created builder + * @since 6.2.13 */ - static Builder with(Collection fragments) { + static Builder fragments(Collection fragments) { return new DefaultFragmentsRenderingBuilder().fragments(fragments); } + /** + * The same as {@link #fragment(String, Map)}. + * @deprecated in favor of {@link #fragment(String, Map)} + */ + @Deprecated(since = "6.2.13", forRemoval = true) + static Builder with(String viewName, Map model) { + return fragment(viewName, model); + } + + /** + * The same as {@link #fragment(String)}. + * @deprecated in favor of {@link #fragment(String)} + */ + @Deprecated(since = "6.2.13", forRemoval = true) + static Builder with(String viewName) { + return fragment(viewName); + } + + /** + * The same as {@link #fragments(Collection)}. + * @deprecated in favor of {@link #fragments(Collection)} + */ + @Deprecated(since = "6.2.13", forRemoval = true) + static Builder with(Collection fragments) { + return fragments(fragments); + } + + /** * Defines a builder for {@link FragmentsRendering}. */ @@ -111,32 +146,32 @@ public interface FragmentsRendering extends SmartView { Builder headers(Consumer headersConsumer); /** - * Add a fragment with a view name and a model. + * Add an HTML fragment. * @param viewName the name of the view for the fragment - * @param model attributes for the fragment in addition to model - * attributes inherited from the shared model for the request + * @param model fragment attributes in addition to attributes from the + * shared model for the request * @return this builder */ Builder fragment(String viewName, Map model); /** - * Variant of {@link #fragment(String, Map)} with a view name only, but - * also inheriting model attributes from the shared model for the request. + * Add an HTML fragment. The fragment will use attributes from the shared + * model for the request. * @param viewName the name of the view for the fragment * @return this builder */ Builder fragment(String viewName); /** - * Variant of {@link #fragment(String, Map)} with a {@link ModelAndView}. - * @param fragment the fragment to add; the fragment also inherits model + * Add an HTML fragment. + * @param fragment the fragment to add; the fragment also inherits * attributes from the shared model for the request * @return this builder */ Builder fragment(ModelAndView fragment); /** - * Variant of {@link #fragment(String, Map)} with a collection of {@link ModelAndView}s. + * Add multiple HTML fragments. * @param fragments the fragments to add; each fragment also inherits model * attributes from the shared model for the request * @return this builder diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandlerTests.java index 858ed5e5d83..af78c51f3d2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandlerTests.java @@ -91,7 +91,7 @@ class ModelAndViewMethodReturnValueHandlerTests { @Test void handleFragmentsRendering() throws Exception { - FragmentsRendering rendering = FragmentsRendering.with("viewName").build(); + FragmentsRendering rendering = FragmentsRendering.fragment("viewName").build(); handler.handleReturnValue(rendering, returnParamModelAndView, mavContainer, webRequest); assertThat(mavContainer.getView()).isInstanceOf(SmartView.class); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingTests.java index 015d98a2a69..267a38b681b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultFragmentsRenderingTests.java @@ -56,7 +56,7 @@ public class DefaultFragmentsRenderingTests { MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); - FragmentsRendering view = FragmentsRendering.with("fragment1", Map.of("foo", "Foo")) + FragmentsRendering view = FragmentsRendering.fragment("fragment1", Map.of("foo", "Foo")) .fragment("fragment2", Map.of("bar", "Bar")) .header("headerName", "headerValue") .build();