Skip to content

Commit 700f463

Browse files
caaladormshabarov
authored andcommitted
feat: Enable preserving partial view chain
Cherry pick of #21189
1 parent 9346d9e commit 700f463

File tree

14 files changed

+629
-45
lines changed

14 files changed

+629
-45
lines changed

flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,19 @@ public boolean cancelPendingTitleUpdate() {
731731
return result;
732732
}
733733

734+
/**
735+
* Populate the routerTargetChain with RouterLayouts, but only if the target
736+
* chain is empty. If the chain contains elements the given list is ignored.
737+
*
738+
* @param layouts
739+
* stored router target chain to set as last navigated chain
740+
*/
741+
public void setRouterTargetChain(List<RouterLayout> layouts) {
742+
if (routerTargetChain.isEmpty()) {
743+
routerTargetChain.addAll(layouts);
744+
}
745+
}
746+
734747
/**
735748
* Shows a route target in the related UI. This method is intended for
736749
* framework use only. Use {@link UI#navigate(String)} to change the route

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,19 @@
3838
@Inherited
3939
@Documented
4040
public @interface PreserveOnRefresh {
41+
42+
/**
43+
* Set to true if refresh should also reuse partial chain components of
44+
* stored view chain.
45+
* <p>
46+
* This means that when navigating from a preserve on refresh target to a
47+
* new url in the same client window context, where windowName matches, the
48+
* router layouts that have been preserved will be reused without
49+
* re-creation for the new route.
50+
* <p>
51+
* Default is {@code false} so only url match is repopulated.
52+
*
53+
* @return {@code true} if partial chain match should be checked and used
54+
*/
55+
boolean partialMatch() default false;
4156
}

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

Lines changed: 145 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public NavigationState getNavigationState() {
119119
*/
120120
@SuppressWarnings("unchecked")
121121
// Non-private for testing purposes
122-
static <T extends HasElement> T getRouteTarget(Class<T> routeTargetType,
122+
<T extends HasElement> T getRouteTarget(Class<T> routeTargetType,
123123
NavigationEvent event) {
124124
UI ui = event.getUI();
125125
Optional<HasElement> currentInstance = ui.getInternals()
@@ -160,34 +160,13 @@ public int handle(NavigationEvent event) {
160160
return result.get();
161161
}
162162

163-
final ArrayList<HasElement> chain;
163+
final ArrayList<HasElement> chain = new ArrayList<>();
164164

165165
final boolean preserveOnRefreshTarget = isPreserveOnRefreshTarget(
166166
routeTargetType, routeLayoutTypes);
167167

168-
if (preserveOnRefreshTarget) {
169-
final Optional<ArrayList<HasElement>> maybeChain = getPreservedChain(
170-
event);
171-
if (!maybeChain.isPresent()) {
172-
// We're returning because the preserved chain is not ready to
173-
// be used as is, and requires client data requested within
174-
// `getPreservedChain`. Once the data is retrieved from the
175-
// client, `handle` method will be invoked with the same
176-
// `NavigationEvent` argument.
177-
return HttpStatusCode.OK.getCode();
178-
} else {
179-
chain = maybeChain.get();
180-
}
181-
} else {
182-
183-
// Create an empty chain which gets populated later in
184-
// `createChainIfEmptyAndExecuteBeforeEnterNavigation`.
185-
chain = new ArrayList<>();
186-
187-
// Has any preserved components already been created here? If so,
188-
// we don't want to navigate back to them ever so clear cache for
189-
// window.
190-
clearAllPreservedChains(ui);
168+
if (populateChain(chain, preserveOnRefreshTarget, event)) {
169+
return HttpStatusCode.OK.getCode();
191170
}
192171

193172
// Set navigationTrigger to RELOAD if this is a refresh of a preserve
@@ -248,6 +227,94 @@ public int handle(NavigationEvent event) {
248227
return statusCode;
249228
}
250229

230+
/**
231+
* Populate element chain from a preserved chain or give clean chain to be
232+
* populated.
233+
*
234+
* @param chain
235+
* chain to populate
236+
* @param preserveOnRefreshTarget
237+
* preserve on refresh boolean
238+
* @param event
239+
* current navigation event
240+
* @return {@code true} if additional client data requested, else
241+
* {@code false}
242+
*/
243+
private boolean populateChain(ArrayList<HasElement> chain,
244+
boolean preserveOnRefreshTarget, NavigationEvent event) {
245+
if (preserveOnRefreshTarget) {
246+
final Optional<ArrayList<HasElement>> maybeChain = getPreservedChain(
247+
event);
248+
if (!maybeChain.isPresent()) {
249+
// We're returning because the preserved chain is not ready to
250+
// be used as is, and requires client data requested within
251+
// `getPreservedChain`. Once the data is retrieved from the
252+
// client, `handle` method will be invoked with the same
253+
// `NavigationEvent` argument.
254+
return true;
255+
} else {
256+
chain.addAll(maybeChain.get());
257+
// If partialMatch is set to true check if the cache contains a
258+
// chain and possibly request extended details to get window
259+
// name
260+
// to select cached chain.
261+
if (chain.isEmpty() && isPreservePartialTarget(
262+
navigationState.getNavigationTarget(),
263+
routeLayoutTypes)) {
264+
UI ui = event.getUI();
265+
if (ui.getInternals().getExtendedClientDetails() == null) {
266+
PreservedComponentCache cache = ui.getSession()
267+
.getAttribute(PreservedComponentCache.class);
268+
if (cache != null && !cache.isEmpty()) {
269+
// As there is a cached chain we get the client
270+
// details
271+
// to get the window name so we can determine if the
272+
// cache contains a chain for us to use.
273+
ui.getPage().retrieveExtendedClientDetails(
274+
details -> handle(event));
275+
return true;
276+
}
277+
} else {
278+
Optional<List<HasElement>> partialChain = getWindowPreservedChain(
279+
ui.getSession(),
280+
ui.getInternals().getExtendedClientDetails()
281+
.getWindowName());
282+
if (partialChain.isPresent()) {
283+
List<HasElement> oldChain = partialChain.get();
284+
disconnectElements(oldChain, ui);
285+
286+
List<RouterLayout> routerLayouts = new ArrayList<>();
287+
288+
for (HasElement hasElement : oldChain) {
289+
if (hasElement instanceof RouterLayout) {
290+
routerLayouts
291+
.add((RouterLayout) hasElement);
292+
} else {
293+
// Remove any non element from their parent
294+
// to
295+
// not get old or duplicate route content
296+
hasElement.getElement().removeFromParent();
297+
}
298+
}
299+
ui.getInternals()
300+
.setRouterTargetChain(routerLayouts);
301+
}
302+
}
303+
}
304+
}
305+
} else {
306+
// Create an empty chain which gets populated later in
307+
// `createChainIfEmptyAndExecuteBeforeEnterNavigation`.
308+
chain.clear();
309+
310+
// Has any preserved components already been created here? If so,
311+
// we don't want to navigate back to them ever so clear cache for
312+
// window.
313+
clearAllPreservedChains(event.getUI());
314+
}
315+
return false;
316+
}
317+
251318
private void pushHistoryStateIfNeeded(NavigationEvent event, UI ui) {
252319
if (event instanceof ErrorNavigationEvent) {
253320
ErrorNavigationEvent errorEvent = (ErrorNavigationEvent) event;
@@ -819,31 +886,34 @@ private Optional<ArrayList<HasElement>> getPreservedChain(
819886
if (maybePreserved.isPresent()) {
820887
// Re-use preserved chain for this route
821888
ArrayList<HasElement> chain = maybePreserved.get();
822-
final HasElement root = chain.get(chain.size() - 1);
823-
final Component component = (Component) chain.get(0);
824-
final Optional<UI> maybePrevUI = component.getUI();
825-
826-
if (maybePrevUI.isPresent() && maybePrevUI.get().equals(ui)) {
827-
return Optional.of(chain);
828-
}
829-
830-
// Remove the top-level component from the tree
831-
root.getElement().removeFromTree(false);
832-
833-
// Transfer all remaining UI child elements (typically dialogs
834-
// and notifications) to the new UI
835-
maybePrevUI.ifPresent(prevUi -> {
836-
ui.getInternals().moveElementsFrom(prevUi);
837-
prevUi.close();
838-
});
839-
889+
disconnectElements(chain, ui);
840890
return Optional.of(chain);
841891
}
842892
}
843893

844894
return Optional.of(new ArrayList<>(0));
845895
}
846896

897+
private static void disconnectElements(List<HasElement> chain, UI ui) {
898+
final HasElement root = chain.get(chain.size() - 1);
899+
final Component component = (Component) chain.get(0);
900+
final Optional<UI> maybePrevUI = component.getUI();
901+
902+
if (maybePrevUI.isPresent() && maybePrevUI.get().equals(ui)) {
903+
return;
904+
}
905+
906+
// Remove the top-level component from the tree
907+
root.getElement().removeFromTree(false);
908+
909+
// Transfer all remaining UI child elements (typically dialogs
910+
// and notifications) to the new UI
911+
maybePrevUI.ifPresent(prevUi -> {
912+
ui.getInternals().moveElementsFrom(prevUi);
913+
prevUi.close();
914+
});
915+
}
916+
847917
/**
848918
* Invoke this method with the chain that needs to be preserved after
849919
* {@link #handle(NavigationEvent)} method created it.
@@ -932,6 +1002,18 @@ private static boolean isPreserveOnRefreshTarget(
9321002
.isAnnotationPresent(PreserveOnRefresh.class));
9331003
}
9341004

1005+
private static boolean isPreservePartialTarget(
1006+
Class<? extends Component> routeTargetType,
1007+
List<Class<? extends RouterLayout>> routeLayoutTypes) {
1008+
return (routeTargetType.isAnnotationPresent(PreserveOnRefresh.class)
1009+
&& routeTargetType.getAnnotation(PreserveOnRefresh.class)
1010+
.partialMatch())
1011+
|| routeLayoutTypes.stream().anyMatch(layoutType -> layoutType
1012+
.isAnnotationPresent(PreserveOnRefresh.class)
1013+
&& layoutType.getAnnotation(PreserveOnRefresh.class)
1014+
.partialMatch());
1015+
}
1016+
9351017
// maps window.name to (location, chain)
9361018
private static class PreservedComponentCache
9371019
extends HashMap<String, Pair<String, ArrayList<HasElement>>> {
@@ -958,9 +1040,27 @@ static Optional<ArrayList<HasElement>> getPreservedChain(
9581040
if (cache != null && cache.containsKey(windowName) && cache
9591041
.get(windowName).getFirst().equals(location.getPath())) {
9601042
return Optional.of(cache.get(windowName).getSecond());
961-
} else {
962-
return Optional.empty();
9631043
}
1044+
return Optional.empty();
1045+
}
1046+
1047+
/**
1048+
* Get a preserved chain by window name only ignoring location path.
1049+
*
1050+
* @param session
1051+
* current session
1052+
* @param windowName
1053+
* window name to get cached view stack for
1054+
* @return view stack cache if available for window name
1055+
*/
1056+
static Optional<List<HasElement>> getWindowPreservedChain(
1057+
VaadinSession session, String windowName) {
1058+
final PreservedComponentCache cache = session
1059+
.getAttribute(PreservedComponentCache.class);
1060+
if (cache != null && cache.containsKey(windowName)) {
1061+
return Optional.of(cache.get(windowName).getSecond());
1062+
}
1063+
return Optional.empty();
9641064
}
9651065

9661066
static void setPreservedChain(VaadinSession session, String windowName,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2000-2025 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+
17+
package com.vaadin.flow.misc.ui.partial;
18+
19+
import com.vaadin.flow.component.html.Div;
20+
import com.vaadin.flow.component.html.NativeButton;
21+
import com.vaadin.flow.router.ParentLayout;
22+
import com.vaadin.flow.router.RouterLayout;
23+
24+
@ParentLayout(RootLayout.class)
25+
public class MainLayout extends Div implements RouterLayout {
26+
27+
public static final String EVENT_LOG_ID = "event-log";
28+
public static final String RESET_ID = "reset-log";
29+
30+
private static int eventCounter = 0;
31+
32+
private final Div log = new Div();
33+
34+
public MainLayout() {
35+
log.setText(++eventCounter + ": " + getClass().getSimpleName()
36+
+ ": constructor");
37+
log.setId(EVENT_LOG_ID);
38+
NativeButton reset = new NativeButton("Reset count",
39+
e -> eventCounter = 0);
40+
reset.setId(RESET_ID);
41+
add(log, reset);
42+
}
43+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2000-2025 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+
17+
package com.vaadin.flow.misc.ui.partial;
18+
19+
import com.vaadin.flow.component.html.Div;
20+
import com.vaadin.flow.router.Route;
21+
import com.vaadin.flow.router.RouterLink;
22+
23+
@Route(value = "main", layout = MainLayout.class)
24+
public class MainView extends Div {
25+
26+
public MainView() {
27+
28+
add(new RouterLink("Navigate to second view - this works correctly",
29+
SecondView.class));
30+
}
31+
}

0 commit comments

Comments
 (0)