Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.Router;
import com.vaadin.flow.router.RouterLayout;
import com.vaadin.flow.router.internal.AbstractNavigationStateRenderer;
import com.vaadin.flow.router.internal.AfterNavigationHandler;
import com.vaadin.flow.router.internal.BeforeEnterHandler;
import com.vaadin.flow.router.internal.BeforeLeaveHandler;
Expand Down Expand Up @@ -738,6 +739,8 @@ public void setTitle(String title) {
addJavaScriptInvocation(pendingTitleUpdateCanceler);

this.title = title;
AbstractNavigationStateRenderer.updatePreservedChainTitle(getUI(),
title);
}

private String generateTitleScript() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.vaadin.flow.router.internal;

import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -38,7 +39,6 @@
import com.vaadin.flow.component.page.ExtendedClientDetails;
import com.vaadin.flow.di.Instantiator;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.Pair;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.internal.menu.MenuRegistry;
Expand Down Expand Up @@ -256,17 +256,24 @@ public int handle(NavigationEvent event) {
private boolean populateChain(ArrayList<HasElement> chain,
boolean preserveOnRefreshTarget, NavigationEvent event) {
if (preserveOnRefreshTarget && !event.isForceInstantiation()) {
final Optional<ArrayList<HasElement>> maybeChain = getPreservedChain(
final Optional<PreservedViewData> maybePreserved = getPreservedChain(
event);
if (maybeChain.isEmpty()) {
if (maybePreserved.isEmpty()) {
// We're returning because the preserved chain is not ready to
// be used as is, and requires client data requested within
// `getPreservedChain`. Once the data is retrieved from the
// client, `handle` method will be invoked with the same
// `NavigationEvent` argument.
return true;
}
chain.addAll(maybeChain.get());

final PreservedViewData preservedData = maybePreserved.get();
chain.addAll(preservedData.chain);

// Restore title if available
if (!chain.isEmpty() && preservedData.title != null) {
event.getUI().getInternals().setTitle(preservedData.title);
}

// If partialMatch is set to true check if the cache contains a
// chain and possibly request extended details to get window name
Expand Down Expand Up @@ -986,7 +993,7 @@ private NavigationEvent getNavigationEvent(NavigationEvent event,
* If the chain is missing and needs to be created this method returns an
* {@link Optional} wrapping an empty {@link ArrayList}.
*/
private Optional<ArrayList<HasElement>> getPreservedChain(
private Optional<PreservedViewData> getPreservedChain(
NavigationEvent event) {
final Location location = event.getLocation();
final UI ui = event.getUI();
Expand All @@ -1004,18 +1011,19 @@ private Optional<ArrayList<HasElement>> getPreservedChain(
}
} else {
final String windowName = details.getWindowName();
final Optional<ArrayList<HasElement>> maybePreserved = getPreservedChain(
final Optional<PreservedViewData> maybePreserved = getPreservedChain(
session, windowName, event.getLocation());
if (maybePreserved.isPresent()) {
// Re-use preserved chain for this route
ArrayList<HasElement> chain = maybePreserved.get();
disconnectElements(chain, ui);
PreservedViewData data = maybePreserved.get();
disconnectElements(data.chain, ui);

return Optional.of(chain);
return Optional.of(data);
}
}

return Optional.of(new ArrayList<>(0));
return Optional
.of(new PreservedViewData(null, new ArrayList<>(0), null));
}

private static void disconnectElements(List<HasElement> chain, UI ui) {
Expand Down Expand Up @@ -1052,16 +1060,19 @@ private void setPreservedChain(ArrayList<HasElement> chain,
final ExtendedClientDetails extendedClientDetails = ui.getInternals()
.getExtendedClientDetails();

final String title = ui.getInternals().getTitle();

if (extendedClientDetails.getWindowName() == null) {
// We need first to retrieve the window name in order to cache the
// component chain for later potential refreshes.
ui.getPage().retrieveExtendedClientDetails(
details -> setPreservedChain(session,
details.getWindowName(), location, chain));
ui.getPage()
.retrieveExtendedClientDetails(details -> setPreservedChain(
session, details.getWindowName(), location, chain,
title));

} else {
final String windowName = extendedClientDetails.getWindowName();
setPreservedChain(session, windowName, location, chain);
setPreservedChain(session, windowName, location, chain, title);
}
}

Expand Down Expand Up @@ -1104,12 +1115,17 @@ private static void updatePageTitle(NavigationEvent navigationEvent,
.map(PageTitle::value).orElse("");

// check for HasDynamicTitle in current router targets chain
String title = RouteUtil.getDynamicTitle(navigationEvent.getUI())
.orElseGet(() -> Optional
.ofNullable(
MenuRegistry.getClientRoutes(true).get(route))
.map(AvailableViewInfo::title)
.orElseGet(lookForTitleInTarget));
Optional<String> dynamicTitle = RouteUtil
.getDynamicTitle(navigationEvent.getUI());
String title = dynamicTitle.orElseGet(() -> Optional
.ofNullable(MenuRegistry.getClientRoutes(true).get(route))
.map(AvailableViewInfo::title).orElseGet(lookForTitleInTarget));

if (navigationEvent.getTrigger() == NavigationTrigger.REFRESH
&& !dynamicTitle.isPresent()
&& navigationEvent.getUI().getInternals().getTitle() != null) {
return;
}

navigationEvent.getUI().getPage().setTitle(title);
}
Expand Down Expand Up @@ -1141,7 +1157,20 @@ private static boolean isPreservePartialTarget(

// maps window.name to (location, chain)
private static class PreservedComponentCache
extends HashMap<String, Pair<String, ArrayList<HasElement>>> {
extends HashMap<String, PreservedViewData> {
}

static class PreservedViewData implements Serializable {
final String location;
final ArrayList<HasElement> chain;
final String title;

PreservedViewData(String location, ArrayList<HasElement> chain,
String title) {
this.location = location;
this.chain = chain;
this.title = title;
}
}

static boolean hasPreservedChain(VaadinSession session) {
Expand All @@ -1155,16 +1184,16 @@ static boolean hasPreservedChainOfLocation(VaadinSession session,
final PreservedComponentCache cache = session
.getAttribute(PreservedComponentCache.class);
return cache != null && cache.values().stream()
.anyMatch(entry -> entry.getFirst().equals(location.getPath()));
.anyMatch(entry -> entry.location.equals(location.getPath()));
}

static Optional<ArrayList<HasElement>> getPreservedChain(
VaadinSession session, String windowName, Location location) {
static Optional<PreservedViewData> getPreservedChain(VaadinSession session,
String windowName, Location location) {
final PreservedComponentCache cache = session
.getAttribute(PreservedComponentCache.class);
if (cache != null && cache.containsKey(windowName) && cache
.get(windowName).getFirst().equals(location.getPath())) {
return Optional.of(cache.get(windowName).getSecond());
if (cache != null && cache.containsKey(windowName)
&& cache.get(windowName).location.equals(location.getPath())) {
return Optional.of(cache.get(windowName));
}
return Optional.empty();
}
Expand All @@ -1183,22 +1212,61 @@ static Optional<List<HasElement>> getWindowPreservedChain(
final PreservedComponentCache cache = session
.getAttribute(PreservedComponentCache.class);
if (cache != null && cache.containsKey(windowName)) {
return Optional.of(cache.get(windowName).getSecond());
return Optional.of(cache.get(windowName).chain);
}
return Optional.empty();
}

static void setPreservedChain(VaadinSession session, String windowName,
Location location, ArrayList<HasElement> chain) {
Location location, ArrayList<HasElement> chain, String title) {
PreservedComponentCache cache = session
.getAttribute(PreservedComponentCache.class);
if (cache == null) {
cache = new PreservedComponentCache();
}
cache.put(windowName, new Pair<>(location.getPath(), chain));
cache.put(windowName,
new PreservedViewData(location.getPath(), chain, title));
session.setAttribute(PreservedComponentCache.class, cache);
}

/**
* Updates the preserved chain title for the current UI.
*
* @param ui
* the UI to update
* @param title
* the new title
*/
public static void updatePreservedChainTitle(UI ui, String title) {
VaadinSession session = ui.getSession();
if (session == null) {
return;
}
if (!hasPreservedChain(session)) {
return;
}

ExtendedClientDetails details = ui.getInternals()
.getExtendedClientDetails();
if (details == null || details.getWindowName() == null) {
return;
}

String windowName = details.getWindowName();
PreservedComponentCache cache = session
.getAttribute(PreservedComponentCache.class);
if (cache != null && cache.containsKey(windowName)) {
PreservedViewData oldData = cache.get(windowName);
Location currentLocation = ui.getInternals()
.getActiveViewLocation();
if (currentLocation != null
&& currentLocation.getPath().equals(oldData.location)) {
cache.put(windowName, new PreservedViewData(oldData.location,
oldData.chain, title));
}
}
}

private static void clearAllPreservedChains(UI ui) {
final VaadinSession session = ui.getSession();
// Note that this check is always false if @PreserveOnRefresh has not
Expand Down Expand Up @@ -1236,7 +1304,7 @@ public static void purgeInactiveUIPreservedChainCache(UI inactiveUI) {
StateNode uiNode = inactiveUI.getElement().getNode();
Set<String> inactiveWindows = cache.entrySet().stream()
.filter(e -> {
ArrayList<HasElement> chain = e.getValue().getSecond();
ArrayList<HasElement> chain = e.getValue().chain;
// chain is never empty
StateNode chainNode = chain.get(0).getElement()
.getNode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,11 @@
navigationStateFromTarget(PreservedView.class));

// given the session has a cache of something at this location
AbstractNavigationStateRenderer.setPreservedChain(session, "",
new Location("preserved"), new ArrayList<>(Collections
.singletonList(Mockito.mock(Component.class))));
AbstractNavigationStateRenderer
.setPreservedChain(session, "", new Location("preserved"),
new ArrayList<>(Collections
.singletonList(Mockito.mock(Component.class))),
null);

// given a UI that contain no window name with an instrumented Page
// that records JS invocations
Expand Down Expand Up @@ -518,7 +520,8 @@
// given the session has a cache of PreservedView at this location
final PreservedView view = new PreservedView();
AbstractNavigationStateRenderer.setPreservedChain(session, "ROOT.123",
new Location("preserved"), new ArrayList<>(List.of(view)));
new Location("preserved"), new ArrayList<>(List.of(view)),
null);

// given an old UI that contains the component and an extra element
MockUI ui0 = new MockUI(session);
Expand Down Expand Up @@ -574,7 +577,7 @@

AbstractNavigationStateRenderer.setPreservedChain(session, "ROOT.123",
new Location("preservedNested"),
new ArrayList<>(Arrays.asList(nestedView, layout)));
new ArrayList<>(Arrays.asList(nestedView, layout)), null);

// given a UI that contain a window name ROOT.123
MockUI ui = new MockUI(session);
Expand Down Expand Up @@ -622,7 +625,7 @@
new Location(path,
new QueryParameters(Collections.singletonMap("a",
Collections.emptyList()))),
new ArrayList<>(List.of(view)));
new ArrayList<>(List.of(view)), null);

ExtendedClientDetails details = Mockito
.mock(ExtendedClientDetails.class);
Expand Down Expand Up @@ -905,7 +908,7 @@
// given a UI with an instrumented Page that records
// getHistory().pushState calls
AtomicBoolean pushStateCalled = new AtomicBoolean(false);
List<Location> pushStateLocations = new ArrayList<>();

Check warning on line 911 in flow-server/src/test/java/com/vaadin/flow/router/internal/NavigationStateRendererTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consume or remove this unused collection

See more on https://sonarcloud.io/project/issues?id=vaadin_flow&issues=AZsN_Dx-yfr418RyEl9L&open=AZsN_Dx-yfr418RyEl9L&pullRequest=22972
MockUI ui = new MockUI(session) {
final Page page = new Page(this) {
final History history = new History(getUI().get()) {
Expand Down Expand Up @@ -1011,21 +1014,23 @@
Location location = new Location("preserved");
AbstractNavigationStateRenderer.setPreservedChain(session, "ACTIVE",
location,
new ArrayList<>(Collections.singletonList(attachedToActiveUI)));
new ArrayList<>(Collections.singletonList(attachedToActiveUI)),
null);
AbstractNavigationStateRenderer.setPreservedChain(session, "INACTIVE",
location, new ArrayList<>(
Collections.singletonList(attachedToInactiveUI)));
Collections.singletonList(attachedToInactiveUI)),
null);

AbstractNavigationStateRenderer
.purgeInactiveUIPreservedChainCache(inActiveUI);

Optional<ArrayList<HasElement>> active = AbstractNavigationStateRenderer
Optional<AbstractNavigationStateRenderer.PreservedViewData> active = AbstractNavigationStateRenderer
.getPreservedChain(session, "ACTIVE", location);
Assert.assertTrue(
"Expected preserved chain for active window to be present",
active.isPresent());

Optional<ArrayList<HasElement>> inactive = AbstractNavigationStateRenderer
Optional<AbstractNavigationStateRenderer.PreservedViewData> inactive = AbstractNavigationStateRenderer
.getPreservedChain(session, "INACTIVE", location);
Assert.assertFalse(
"Expected preserved chain for inactive window to be removed",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2000-2025 Vaadin Ltd.
*
* 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
*
* http://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 com.vaadin.flow.misc.ui;

import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.PreserveOnRefresh;
import com.vaadin.flow.router.Route;

@Route("preserve-on-refresh-title-view")
@PreserveOnRefresh
@PageTitle("Initial Title")
public class PreserveOnRefreshTitleView extends Div {

public PreserveOnRefreshTitleView() {
NativeButton updateTitle = new NativeButton("Update Title", e -> {
getUI().ifPresent(ui -> ui.getPage().setTitle("Updated Title"));
});
updateTitle.setId("update-title");
add(updateTitle);
}
}
Loading
Loading