parent
6ee8786385
commit
14c1faa5ee
|
@ -27,7 +27,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
import org.springframework.web.servlet.SmartView;
|
import org.springframework.web.servlet.SmartView;
|
||||||
import org.springframework.web.servlet.View;
|
import org.springframework.web.servlet.View;
|
||||||
import org.springframework.web.servlet.view.FragmentsView;
|
import org.springframework.web.servlet.view.FragmentsRendering;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles return values of type {@link ModelAndView} copying view and model
|
* Handles return values of type {@link ModelAndView} copying view and model
|
||||||
|
@ -78,7 +78,7 @@ public class ModelAndViewMethodReturnValueHandler implements HandlerMethodReturn
|
||||||
if (Collection.class.isAssignableFrom(type)) {
|
if (Collection.class.isAssignableFrom(type)) {
|
||||||
type = returnType.nested().getNestedParameterType();
|
type = returnType.nested().getNestedParameterType();
|
||||||
}
|
}
|
||||||
return ModelAndView.class.isAssignableFrom(type);
|
return (ModelAndView.class.isAssignableFrom(type) || FragmentsRendering.class.isAssignableFrom(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@ -92,7 +92,11 @@ public class ModelAndViewMethodReturnValueHandler implements HandlerMethodReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnValue instanceof Collection<?> mavs) {
|
if (returnValue instanceof Collection<?> mavs) {
|
||||||
mavContainer.setView(FragmentsView.create((Collection<ModelAndView>) mavs));
|
returnValue = FragmentsRendering.with((Collection<ModelAndView>) mavs).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnValue instanceof FragmentsRendering rendering) {
|
||||||
|
mavContainer.setView(rendering);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.web.servlet.view;
|
package org.springframework.web.servlet.view;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -31,27 +32,23 @@ import jakarta.servlet.http.HttpServletResponseWrapper;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
import org.springframework.web.servlet.SmartView;
|
|
||||||
import org.springframework.web.servlet.View;
|
import org.springframework.web.servlet.View;
|
||||||
import org.springframework.web.servlet.ViewResolver;
|
import org.springframework.web.servlet.ViewResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link View} that enables rendering of a collection of fragments, each with
|
* Default implementation of {@link FragmentsRendering} that can render fragments
|
||||||
* its own view and model, also inheriting common attributes from the top-level model.
|
* through the {@link org.springframework.web.servlet.SmartView} contract.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 6.2
|
* @since 6.2
|
||||||
*/
|
*/
|
||||||
public class FragmentsView implements SmartView {
|
final class DefaultFragmentsRendering implements FragmentsRendering {
|
||||||
|
|
||||||
private final Collection<ModelAndView> modelAndViews;
|
private final Collection<ModelAndView> modelAndViews;
|
||||||
|
|
||||||
|
|
||||||
/**
|
DefaultFragmentsRendering(Collection<ModelAndView> modelAndViews) {
|
||||||
* Protected constructor to allow extension.
|
this.modelAndViews = new ArrayList<>(modelAndViews);
|
||||||
*/
|
|
||||||
protected FragmentsView(Collection<ModelAndView> modelAndViews) {
|
|
||||||
this.modelAndViews = modelAndViews;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,20 +92,9 @@ public class FragmentsView implements SmartView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "FragmentsView " + this.modelAndViews;
|
return "DefaultFragmentsRendering " + this.modelAndViews;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory method to create an instance with the given fragments.
|
|
||||||
* @param modelAndViews the {@link ModelAndView} to use
|
|
||||||
* @return the created {@code FragmentsView} instance
|
|
||||||
*/
|
|
||||||
public static FragmentsView create(Collection<ModelAndView> modelAndViews) {
|
|
||||||
return new FragmentsView(modelAndViews);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.web.servlet.view;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link FragmentsRendering.Builder} implementation that collects the
|
||||||
|
* fragments and creates a {@link DefaultFragmentsRendering}.
|
||||||
|
*
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
* @since 6.2
|
||||||
|
*/
|
||||||
|
final class DefaultFragmentsRenderingBuilder implements FragmentsRendering.Builder {
|
||||||
|
|
||||||
|
private final Collection<ModelAndView> fragments = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DefaultFragmentsRenderingBuilder fragment(String viewName, Map<String, Object> model) {
|
||||||
|
return fragment(new ModelAndView(viewName, model));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DefaultFragmentsRenderingBuilder fragment(String viewName) {
|
||||||
|
return fragment(new ModelAndView(viewName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DefaultFragmentsRenderingBuilder fragment(ModelAndView fragment) {
|
||||||
|
this.fragments.add(fragment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DefaultFragmentsRenderingBuilder fragments(Collection<ModelAndView> fragments) {
|
||||||
|
this.fragments.addAll(fragments);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FragmentsRendering build() {
|
||||||
|
return new DefaultFragmentsRendering(this.fragments);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.web.servlet.view;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
import org.springframework.web.servlet.SmartView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public API for HTML rendering a collection fragments each with its own view
|
||||||
|
* and model. For use with view technologies such as
|
||||||
|
* <a href="https://htmx.org/">htmx</a> where multiple page fragments may be
|
||||||
|
* rendered in a single response. Supported as a return value from a Spring MVC
|
||||||
|
* controller method.
|
||||||
|
*
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
* @since 6.2
|
||||||
|
*/
|
||||||
|
public interface FragmentsRendering extends SmartView {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a builder for {@link FragmentsRendering}, adding a fragment with
|
||||||
|
* the given view name and 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 shared model for the request
|
||||||
|
* @return the created builder
|
||||||
|
*/
|
||||||
|
static Builder with(String viewName, Map<String, Object> model) {
|
||||||
|
return new DefaultFragmentsRenderingBuilder().fragment(viewName, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @return the created builder
|
||||||
|
*/
|
||||||
|
static Builder with(String viewName) {
|
||||||
|
return new DefaultFragmentsRenderingBuilder().fragment(viewName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of {@link #with(String, Map)} with a collection of fragments.
|
||||||
|
* @param fragments the fragments to add; each fragment also inherits model
|
||||||
|
* attributes from the shared model for the request
|
||||||
|
* @return the created builder
|
||||||
|
*/
|
||||||
|
static Builder with(Collection<ModelAndView> fragments) {
|
||||||
|
return new DefaultFragmentsRenderingBuilder().fragments(fragments);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a builder for {@link FragmentsRendering}.
|
||||||
|
*/
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 shared model for the request
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
|
Builder fragment(String viewName, Map<String, Object> model);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a fragment with a view name only, inheriting model attributes from
|
||||||
|
* the model for the request.
|
||||||
|
* @param viewName the name of the view for the fragment
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
|
Builder fragment(String viewName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a fragment.
|
||||||
|
* @param fragment the fragment to add; the fragment also inherits model
|
||||||
|
* attributes from the shared model for the request
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
|
Builder fragment(ModelAndView fragment);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a collection of fragments.
|
||||||
|
* @param fragments the fragments to add; each fragment also inherits model
|
||||||
|
* attributes from the shared model for the request
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
|
Builder fragments(Collection<ModelAndView> fragments);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a {@link FragmentsRendering} instance.
|
||||||
|
*/
|
||||||
|
FragmentsRendering build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,6 +17,8 @@
|
||||||
package org.springframework.web.servlet.mvc.method.annotation;
|
package org.springframework.web.servlet.mvc.method.annotation;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -26,7 +28,9 @@ import org.springframework.ui.ModelMap;
|
||||||
import org.springframework.web.context.request.ServletWebRequest;
|
import org.springframework.web.context.request.ServletWebRequest;
|
||||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
import org.springframework.web.servlet.ModelAndView;
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
import org.springframework.web.servlet.SmartView;
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap;
|
import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap;
|
||||||
|
import org.springframework.web.servlet.view.FragmentsRendering;
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||||
|
|
||||||
|
@ -60,6 +64,9 @@ class ModelAndViewMethodReturnValueHandlerTests {
|
||||||
@Test
|
@Test
|
||||||
void supportsReturnType() throws Exception {
|
void supportsReturnType() throws Exception {
|
||||||
assertThat(handler.supportsReturnType(returnParamModelAndView)).isTrue();
|
assertThat(handler.supportsReturnType(returnParamModelAndView)).isTrue();
|
||||||
|
assertThat(handler.supportsReturnType(getReturnValueParam("fragmentsRendering"))).isTrue();
|
||||||
|
assertThat(handler.supportsReturnType(getReturnValueParam("fragmentsCollection"))).isTrue();
|
||||||
|
|
||||||
assertThat(handler.supportsReturnType(getReturnValueParam("viewName"))).isFalse();
|
assertThat(handler.supportsReturnType(getReturnValueParam("viewName"))).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +88,22 @@ class ModelAndViewMethodReturnValueHandlerTests {
|
||||||
assertThat(mavContainer.getModel().get("attrName")).isEqualTo("attrValue");
|
assertThat(mavContainer.getModel().get("attrName")).isEqualTo("attrValue");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleFragmentsRendering() throws Exception {
|
||||||
|
FragmentsRendering rendering = FragmentsRendering.with("viewName").build();
|
||||||
|
|
||||||
|
handler.handleReturnValue(rendering, returnParamModelAndView, mavContainer, webRequest);
|
||||||
|
assertThat(mavContainer.getView()).isInstanceOf(SmartView.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleFragmentsCollection() throws Exception {
|
||||||
|
Collection<ModelAndView> fragments = List.of(new ModelAndView("viewName"));
|
||||||
|
|
||||||
|
handler.handleReturnValue(fragments, returnParamModelAndView, mavContainer, webRequest);
|
||||||
|
assertThat(mavContainer.getView()).isInstanceOf(SmartView.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void handleNull() throws Exception {
|
void handleNull() throws Exception {
|
||||||
handler.handleReturnValue(null, returnParamModelAndView, mavContainer, webRequest);
|
handler.handleReturnValue(null, returnParamModelAndView, mavContainer, webRequest);
|
||||||
|
@ -173,4 +196,14 @@ class ModelAndViewMethodReturnValueHandlerTests {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
FragmentsRendering fragmentsRendering() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
Collection<ModelAndView> fragmentsCollection() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,12 +38,12 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.condition.JRE.JAVA_21;
|
import static org.junit.jupiter.api.condition.JRE.JAVA_21;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for rendering through {@link FragmentsView}.
|
* Tests for rendering through {@link DefaultFragmentsRendering}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
*/
|
*/
|
||||||
@DisabledForJreRange(min = JAVA_21, disabledReason = "Kotlin doesn't support Java 21+ yet")
|
@DisabledForJreRange(min = JAVA_21, disabledReason = "Kotlin doesn't support Java 21+ yet")
|
||||||
public class FragmentsViewTests {
|
public class DefaultFragmentsRenderingTests {
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -59,7 +59,7 @@ public class FragmentsViewTests {
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
|
||||||
FragmentsView view = FragmentsView.create(List.of(
|
DefaultFragmentsRendering view = new DefaultFragmentsRendering(List.of(
|
||||||
new ModelAndView("fragment1", Map.of("foo", "Foo")),
|
new ModelAndView("fragment1", Map.of("foo", "Foo")),
|
||||||
new ModelAndView("fragment2", Map.of("bar", "Bar"))));
|
new ModelAndView("fragment2", Map.of("bar", "Bar"))));
|
||||||
|
|
Loading…
Reference in New Issue