Skip to content

Commit

Permalink
Updates to WebMVC fragment rendering API
Browse files Browse the repository at this point in the history
  • Loading branch information
rstoyanchev committed Jul 10, 2024
1 parent 6ee8786 commit 14c1faa
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.SmartView;
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
Expand Down Expand Up @@ -78,7 +78,7 @@ public boolean supportsReturnType(MethodParameter returnType) {
if (Collection.class.isAssignableFrom(type)) {
type = returnType.nested().getNestedParameterType();
}
return ModelAndView.class.isAssignableFrom(type);
return (ModelAndView.class.isAssignableFrom(type) || FragmentsRendering.class.isAssignableFrom(type));
}

@SuppressWarnings("unchecked")
Expand All @@ -92,7 +92,11 @@ public void handleReturnValue(@Nullable Object returnValue, MethodParameter retu
}

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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.springframework.web.servlet.view;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
Expand All @@ -31,27 +32,23 @@
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.SmartView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;

/**
* {@link View} that enables rendering of a collection of fragments, each with
* its own view and model, also inheriting common attributes from the top-level model.
* Default implementation of {@link FragmentsRendering} that can render fragments
* through the {@link org.springframework.web.servlet.SmartView} contract.
*
* @author Rossen Stoyanchev
* @since 6.2
*/
public class FragmentsView implements SmartView {
final class DefaultFragmentsRendering implements FragmentsRendering {

private final Collection<ModelAndView> modelAndViews;


/**
* Protected constructor to allow extension.
*/
protected FragmentsView(Collection<ModelAndView> modelAndViews) {
this.modelAndViews = modelAndViews;
DefaultFragmentsRendering(Collection<ModelAndView> modelAndViews) {
this.modelAndViews = new ArrayList<>(modelAndViews);
}


Expand Down Expand Up @@ -95,20 +92,9 @@ public void render(
}
}


@Override
public String toString() {
return "FragmentsView " + 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);
return "DefaultFragmentsRendering " + this.modelAndViews;
}


Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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();

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package org.springframework.web.servlet.mvc.method.annotation;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -26,7 +28,9 @@
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
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.view.FragmentsRendering;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;

Expand Down Expand Up @@ -60,6 +64,9 @@ void setup() throws Exception {
@Test
void supportsReturnType() throws Exception {
assertThat(handler.supportsReturnType(returnParamModelAndView)).isTrue();
assertThat(handler.supportsReturnType(getReturnValueParam("fragmentsRendering"))).isTrue();
assertThat(handler.supportsReturnType(getReturnValueParam("fragmentsCollection"))).isTrue();

assertThat(handler.supportsReturnType(getReturnValueParam("viewName"))).isFalse();
}

Expand All @@ -81,6 +88,22 @@ void handleViewInstance() throws Exception {
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
void handleNull() throws Exception {
handler.handleReturnValue(null, returnParamModelAndView, mavContainer, webRequest);
Expand Down Expand Up @@ -173,4 +196,14 @@ String viewName() {
return null;
}

@SuppressWarnings("unused")
FragmentsRendering fragmentsRendering() {
return null;
}

@SuppressWarnings("unused")
Collection<ModelAndView> fragmentsCollection() {
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@
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
*/
@DisabledForJreRange(min = JAVA_21, disabledReason = "Kotlin doesn't support Java 21+ yet")
public class FragmentsViewTests {
public class DefaultFragmentsRenderingTests {


@Test
Expand All @@ -59,7 +59,7 @@ void render() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
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("fragment2", Map.of("bar", "Bar"))));

Expand Down

0 comments on commit 14c1faa

Please sign in to comment.