Skip to content

Commit 9c98fbc

Browse files
vaadin-botmcollovatiArtur-
authored
fix: Propagate forceInstantiation and recreateLayoutChain flags through forward/reroute (#23848) (#23955) (#23962)
When refreshCurrentRoute(true) triggers a BeforeEnterObserver that calls forwardTo() or rerouteTo(), the forceInstantiation and recreateLayoutChain flags were lost because the redirect navigation events were created without them. This caused the redirect target to reuse existing component instances instead of creating new ones. Propagate the flags in three locations: - AbstractNavigationStateRenderer.getNavigationEvent() for both normal and error redirect paths - InternalRedirectHandler.handle() for internal redirects Fixes #20988 Co-authored-by: Marco Collovati <marco@vaadin.com> Co-authored-by: Artur Signell <artur@vaadin.com>
1 parent 963a7f8 commit 9c98fbc

8 files changed

Lines changed: 403 additions & 3 deletions

File tree

flow-server/src/main/java/com/vaadin/flow/router/ErrorNavigationEvent.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,35 @@ public ErrorNavigationEvent(Router router, Location location, UI ui,
106106
this.errorParameter = errorParameter;
107107
}
108108

109+
/**
110+
* Creates a new navigation event with force instantiation flags.
111+
*
112+
* @param router
113+
* the router handling the navigation, not {@code null}
114+
* @param location
115+
* the new location, not {@code null}
116+
* @param ui
117+
* the UI in which the navigation occurs, not {@code null}
118+
* @param trigger
119+
* the type of user action that triggered this navigation event,
120+
* not {@code null}
121+
* @param errorParameter
122+
* parameter containing navigation error information
123+
* @param forceInstantiation
124+
* if set to {@code true}, the navigation target will always be
125+
* instantiated
126+
* @param recreateLayoutChain
127+
* if set to {@code true}, the complete layout chain up to the
128+
* navigation target will be re-instantiated
129+
*/
130+
public ErrorNavigationEvent(Router router, Location location, UI ui,
131+
NavigationTrigger trigger, ErrorParameter<?> errorParameter,
132+
boolean forceInstantiation, boolean recreateLayoutChain) {
133+
super(router, location, ui, trigger, (BaseJsonNode) null, false,
134+
forceInstantiation, recreateLayoutChain);
135+
this.errorParameter = errorParameter;
136+
}
137+
109138
/**
110139
* Gets the ErrorParameter if set.
111140
*

flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,9 @@ private NavigationEvent getNavigationEvent(NavigationEvent event,
931931

932932
return new ErrorNavigationEvent(event.getSource(),
933933
event.getLocation(), event.getUI(),
934-
NavigationTrigger.PROGRAMMATIC, errorParameter);
934+
NavigationTrigger.PROGRAMMATIC, errorParameter,
935+
event.isForceInstantiation(),
936+
event.isRecreateLayoutChain());
935937
}
936938

937939
String url;
@@ -972,7 +974,8 @@ private NavigationEvent getNavigationEvent(NavigationEvent event,
972974
Location location = new Location(url, queryParameters);
973975

974976
return new NavigationEvent(event.getSource(), location, event.getUI(),
975-
NavigationTrigger.PROGRAMMATIC, (BaseJsonNode) null, true);
977+
NavigationTrigger.PROGRAMMATIC, (BaseJsonNode) null, true,
978+
event.isForceInstantiation(), event.isRecreateLayoutChain());
976979
}
977980

978981
/**

flow-server/src/main/java/com/vaadin/flow/router/internal/InternalRedirectHandler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public int handle(NavigationEvent event) {
5555
}
5656

5757
return router.navigate(ui, target, event.getTrigger(),
58-
event.getState().orElse(null));
58+
event.getState().orElse(null), event.isForceInstantiation(),
59+
event.isRecreateLayoutChain());
5960
}
6061
}

flow-server/src/test/java/com/vaadin/flow/router/internal/NavigationStateRendererTest.java

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ public void init() {
166166
RouteRegistry registry = ApplicationRouteRegistry
167167
.getInstance(new MockVaadinContext());
168168
router = new Router(registry);
169+
170+
RouteParentLayout.creationCount.set(0);
171+
ConditionalForwardView.shouldForward = false;
172+
ConditionalRerouteView.shouldReroute = false;
169173
}
170174

171175
@Test
@@ -302,7 +306,10 @@ public DeploymentConfiguration getConfiguration() {
302306
@Tag("div")
303307
private static class RouteParentLayout extends Component
304308
implements RouterLayout {
309+
private static final AtomicInteger creationCount = new AtomicInteger(0);
310+
305311
RouteParentLayout() {
312+
creationCount.incrementAndGet();
306313
addAttachListener(e -> layoutAttachCount.getAndIncrement());
307314
layoutUUID = UUID.randomUUID().toString();
308315
}
@@ -327,6 +334,44 @@ private static class SingleView extends Component {
327334
}
328335
}
329336

337+
@Route(value = "forward-target", layout = RouteParentLayout.class)
338+
@Tag("div")
339+
private static class ForwardTargetView extends Component {
340+
}
341+
342+
@Route(value = "reroute-target", layout = RouteParentLayout.class)
343+
@Tag("div")
344+
private static class RerouteTargetView extends Component {
345+
}
346+
347+
@Route(value = "conditional-forward", layout = RouteParentLayout.class)
348+
@Tag("div")
349+
private static class ConditionalForwardView extends Component
350+
implements BeforeEnterObserver {
351+
static boolean shouldForward = false;
352+
353+
@Override
354+
public void beforeEnter(BeforeEnterEvent event) {
355+
if (shouldForward) {
356+
event.forwardTo(ForwardTargetView.class);
357+
}
358+
}
359+
}
360+
361+
@Route(value = "conditional-reroute", layout = RouteParentLayout.class)
362+
@Tag("div")
363+
private static class ConditionalRerouteView extends Component
364+
implements BeforeEnterObserver {
365+
static boolean shouldReroute = false;
366+
367+
@Override
368+
public void beforeEnter(BeforeEnterEvent event) {
369+
if (shouldReroute) {
370+
event.rerouteTo(RerouteTargetView.class);
371+
}
372+
}
373+
}
374+
330375
@Route(value = "/:samplePersonID?/:action?(edit)")
331376
@RouteAlias(value = "")
332377
@Tag("div")
@@ -767,6 +812,80 @@ public void handle_normalView_refreshCurrentRouteRecreatesComponents() {
767812

768813
}
769814

815+
@Test
816+
public void handle_refreshCurrentRoute_withForwardTo_recreatesComponents() {
817+
layoutAttachCount = new AtomicInteger();
818+
viewAttachCount = new AtomicInteger();
819+
820+
MockVaadinServletService service = createMockServiceWithInstantiator();
821+
MockVaadinSession session = new AlwaysLockedVaadinSession(service);
822+
session.setConfiguration(new MockDeploymentConfiguration());
823+
824+
router = session.getService().getRouter();
825+
NavigationStateRenderer renderer = new NavigationStateRenderer(
826+
new NavigationStateBuilder(router)
827+
.withTarget(ConditionalForwardView.class)
828+
.withPath("conditional-forward").build());
829+
router.getRegistry().setRoute("conditional-forward",
830+
ConditionalForwardView.class, List.of(RouteParentLayout.class));
831+
router.getRegistry().setRoute("forward-target", ForwardTargetView.class,
832+
List.of(RouteParentLayout.class));
833+
834+
MockUI ui = new MockUI(session);
835+
836+
// Initial navigation without forward
837+
renderer.handle(
838+
new NavigationEvent(router, new Location("conditional-forward"),
839+
ui, NavigationTrigger.PAGE_LOAD));
840+
841+
ui.getInternals().clearLastHandledNavigation();
842+
843+
// Enable forwarding and refresh with recreateLayoutChain=true
844+
RouteParentLayout.creationCount.set(0);
845+
ConditionalForwardView.shouldForward = true;
846+
ui.refreshCurrentRoute(true);
847+
848+
Assert.assertEquals("Layout should be recreated by both refresh and forward",
849+
2, RouteParentLayout.creationCount.get());
850+
}
851+
852+
@Test
853+
public void handle_refreshCurrentRoute_withRerouteTo_recreatesComponents() {
854+
layoutAttachCount = new AtomicInteger();
855+
viewAttachCount = new AtomicInteger();
856+
857+
MockVaadinServletService service = createMockServiceWithInstantiator();
858+
MockVaadinSession session = new AlwaysLockedVaadinSession(service);
859+
session.setConfiguration(new MockDeploymentConfiguration());
860+
861+
router = session.getService().getRouter();
862+
NavigationStateRenderer renderer = new NavigationStateRenderer(
863+
new NavigationStateBuilder(router)
864+
.withTarget(ConditionalRerouteView.class)
865+
.withPath("conditional-reroute").build());
866+
router.getRegistry().setRoute("conditional-reroute",
867+
ConditionalRerouteView.class, List.of(RouteParentLayout.class));
868+
router.getRegistry().setRoute("reroute-target", RerouteTargetView.class,
869+
List.of(RouteParentLayout.class));
870+
871+
MockUI ui = new MockUI(session);
872+
873+
// Initial navigation without reroute
874+
renderer.handle(
875+
new NavigationEvent(router, new Location("conditional-reroute"),
876+
ui, NavigationTrigger.PAGE_LOAD));
877+
878+
ui.getInternals().clearLastHandledNavigation();
879+
880+
// Enable rerouting and refresh with recreateLayoutChain=true
881+
RouteParentLayout.creationCount.set(0);
882+
ConditionalRerouteView.shouldReroute = true;
883+
ui.refreshCurrentRoute(true);
884+
885+
Assert.assertEquals("Layout should be recreated by both refresh and reroute",
886+
2, RouteParentLayout.creationCount.get());
887+
}
888+
770889
@Test
771890
public void handle_clientNavigation_withMatchingFlowRoute() {
772891
viewAttachCount = new AtomicInteger();

flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/RefreshCurrentRouteLayout.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
import java.util.UUID;
44

5+
import com.vaadin.flow.component.ComponentUtil;
6+
import com.vaadin.flow.component.UI;
57
import com.vaadin.flow.component.html.Div;
68
import com.vaadin.flow.dom.Element;
79
import com.vaadin.flow.router.RouterLayout;
10+
import com.vaadin.flow.uitest.ui.RefreshCurrentRouteRedirectView.RedirectData;
811

912
public class RefreshCurrentRouteLayout implements RouterLayout {
1013

1114
final static String ROUTER_LAYOUT_ID = "routerlayoutid";
15+
final static String LAYOUT_CREATION_COUNT_ID = "layout-creation-count";
1216

1317
private Div layout = new Div();
1418

@@ -17,6 +21,18 @@ public RefreshCurrentRouteLayout() {
1721
Div routerLayoutId = new Div(uniqueId);
1822
routerLayoutId.setId(ROUTER_LAYOUT_ID);
1923
layout.add(routerLayoutId);
24+
25+
UI ui = UI.getCurrent();
26+
if (ui != null) {
27+
RedirectData data = ComponentUtil.getData(ui, RedirectData.class);
28+
if (data != null) {
29+
data.layoutCreationCount++;
30+
Div countDiv = new Div(
31+
String.valueOf(data.layoutCreationCount));
32+
countDiv.setId(LAYOUT_CREATION_COUNT_ID);
33+
layout.add(countDiv);
34+
}
35+
}
2036
}
2137

2238
@Override
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.uitest.ui;
17+
18+
import java.util.UUID;
19+
20+
import com.vaadin.flow.component.html.Div;
21+
import com.vaadin.flow.router.Route;
22+
23+
@Route(value = "com.vaadin.flow.uitest.ui.RefreshCurrentRouteRedirectTargetView", layout = RefreshCurrentRouteLayout.class)
24+
public class RefreshCurrentRouteRedirectTargetView extends Div {
25+
26+
static final String VIEW_ID = "forward-target-id";
27+
28+
public RefreshCurrentRouteRedirectTargetView() {
29+
Div id = new Div(UUID.randomUUID().toString());
30+
id.setId(VIEW_ID);
31+
add(id);
32+
}
33+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.uitest.ui;
17+
18+
import com.vaadin.flow.component.ComponentUtil;
19+
import com.vaadin.flow.component.UI;
20+
import com.vaadin.flow.component.html.Div;
21+
import com.vaadin.flow.component.html.NativeButton;
22+
import com.vaadin.flow.router.BeforeEnterEvent;
23+
import com.vaadin.flow.router.BeforeEnterObserver;
24+
import com.vaadin.flow.router.Route;
25+
26+
@Route(value = "com.vaadin.flow.uitest.ui.RefreshCurrentRouteRedirectView", layout = RefreshCurrentRouteLayout.class)
27+
public class RefreshCurrentRouteRedirectView extends Div
28+
implements BeforeEnterObserver {
29+
30+
enum RedirectMode {
31+
FORWARD, REROUTE
32+
}
33+
34+
static class RedirectData {
35+
RedirectMode mode;
36+
int layoutCreationCount;
37+
38+
RedirectData(RedirectMode mode) {
39+
this.mode = mode;
40+
this.layoutCreationCount = 0;
41+
}
42+
}
43+
44+
static final String FORWARD_AND_REFRESH_LAYOUTS = "forward-refresh-layouts";
45+
static final String FORWARD_AND_REFRESH = "forward-refresh";
46+
static final String REROUTE_AND_REFRESH_LAYOUTS = "reroute-refresh-layouts";
47+
static final String REROUTE_AND_REFRESH = "reroute-refresh";
48+
49+
@Override
50+
public void beforeEnter(BeforeEnterEvent event) {
51+
RedirectData data = ComponentUtil.getData(event.getUI(),
52+
RedirectData.class);
53+
if (data == null) {
54+
return;
55+
}
56+
switch (data.mode) {
57+
case FORWARD ->
58+
event.forwardTo(RefreshCurrentRouteRedirectTargetView.class);
59+
case REROUTE ->
60+
event.rerouteTo(RefreshCurrentRouteRedirectTargetView.class);
61+
}
62+
}
63+
64+
public RefreshCurrentRouteRedirectView() {
65+
addButton(FORWARD_AND_REFRESH_LAYOUTS, "Forward + refresh layouts",
66+
RedirectMode.FORWARD, true);
67+
addButton(FORWARD_AND_REFRESH, "Forward + refresh view only",
68+
RedirectMode.FORWARD, false);
69+
addButton(REROUTE_AND_REFRESH_LAYOUTS, "Reroute + refresh layouts",
70+
RedirectMode.REROUTE, true);
71+
addButton(REROUTE_AND_REFRESH, "Reroute + refresh view only",
72+
RedirectMode.REROUTE, false);
73+
}
74+
75+
private void addButton(String id, String text, RedirectMode mode,
76+
boolean recreateLayouts) {
77+
NativeButton button = new NativeButton(text, e -> {
78+
UI ui = UI.getCurrent();
79+
ComponentUtil.setData(ui, RedirectData.class,
80+
new RedirectData(mode));
81+
ui.refreshCurrentRoute(recreateLayouts);
82+
});
83+
button.setId(id);
84+
add(button);
85+
}
86+
}

0 commit comments

Comments
 (0)