Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor navigation state renderer #20777

Merged
merged 1 commit into from
Jan 3, 2025
Merged
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 @@ -114,7 +114,7 @@ public NavigationState getNavigationState() {
* <p>
* Override this method to control the creation of view instances.
* <p>
* By default always creates new instances.
* By default, always creates new instances.
*
* @param <T>
* the route target type
Expand Down Expand Up @@ -168,56 +168,25 @@ public int handle(NavigationEvent event) {
clearContinueNavigationAction(ui);
checkForDuplicates(routeTargetType, routeLayoutTypes);

BeforeLeaveEvent beforeNavigationDeactivating = new BeforeLeaveEvent(
event, routeTargetType, parameters, routeLayoutTypes);

Optional<Integer> result = executeBeforeLeaveNavigation(event,
beforeNavigationDeactivating);
Optional<Integer> result = handleBeforeLeaveEvents(event,
routeTargetType, parameters);

if (result.isPresent()) {
return result.get();
}

// If navigation target is Hilla route, terminate Flow navigation logic
// here.
String route = event.getLocation().getPath().isEmpty()
? event.getLocation().getPath()
: event.getLocation().getPath().startsWith("/")
? event.getLocation().getPath()
: "/" + event.getLocation().getPath();
if (MenuRegistry.hasClientRoute(route, true) && !MenuRegistry
.getClientRoutes(true).get(route).flowLayout()) {
String route = getFormattedRoute(event);
if (isClientHandled(route)) {
return HttpStatusCode.OK.getCode();
}

final ArrayList<HasElement> chain;
final ArrayList<HasElement> chain = new ArrayList<>();

final boolean preserveOnRefreshTarget = isPreserveOnRefreshTarget(
routeTargetType, routeLayoutTypes);

if (preserveOnRefreshTarget && !event.isForceInstantiation()) {
final Optional<ArrayList<HasElement>> maybeChain = getPreservedChain(
event);
if (maybeChain.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 HttpStatusCode.OK.getCode();
} else {
chain = maybeChain.get();
}
} else {

// Create an empty chain which gets populated later in
// `createChainIfEmptyAndExecuteBeforeEnterNavigation`.
chain = new ArrayList<>();

// Has any preserved components already been created here? If so,
// we don't want to navigate back to them ever so clear cache for
// window.
clearAllPreservedChains(ui);
if (populateChain(chain, preserveOnRefreshTarget, event)) {
return HttpStatusCode.OK.getCode();
}

// Set navigationTrigger to RELOAD if this is a refresh of a preserve
Expand All @@ -234,11 +203,8 @@ public int handle(NavigationEvent event) {
// See https://github.com/vaadin/flow/issues/3619 for more info.
pushHistoryStateIfNeeded(event, ui);

BeforeEnterEvent beforeNavigationActivating = new BeforeEnterEvent(
event, routeTargetType, parameters, routeLayoutTypes);

result = createChainIfEmptyAndExecuteBeforeEnterNavigation(
beforeNavigationActivating, event, chain);
result = handleBeforeNavigationEvents(event, routeTargetType,
parameters, chain);
if (result.isPresent()) {
return result.get();
}
Expand All @@ -255,18 +221,7 @@ public int handle(NavigationEvent event) {
List<RouterLayout> routerLayouts = (List<RouterLayout>) (List<?>) chain
.subList(1, chain.size());

// If a route refresh has been requested, remove all modal components.
// This is necessary because maintaining the correct modality
// cardinality and order is not feasible without knowing who opened them
// and when.
if (ui.hasModalComponent()
&& event.getTrigger() == NavigationTrigger.REFRESH_ROUTE) {
Component modalComponent;
while ((modalComponent = ui.getInternals()
.getActiveModalComponent()) != null) {
modalComponent.removeFromParent();
}
}
cleanModalComponents(event);

// Change the UI according to the navigation Component chain.
ui.getInternals().showRouteTarget(event.getLocation(),
Expand All @@ -276,6 +231,109 @@ public int handle(NavigationEvent event) {
validateStatusCode(statusCode, routeTargetType);

// After navigation event
handleAfterNavigationEvents(ui, parameters);

updatePageTitle(event, componentInstance, route);

return statusCode;
}

/**
* Populate element chain from a preserved chain or give clean chain to be
* populated.
*
* @param chain
* chain to populate
* @param preserveOnRefreshTarget
* preserve on refresh boolean
* @param event
* current navigation event
* @return {@code true} if additional client data requested, else
* {@code false}
*/
private boolean populateChain(ArrayList<HasElement> chain,
boolean preserveOnRefreshTarget, NavigationEvent event) {
if (preserveOnRefreshTarget && !event.isForceInstantiation()) {
final Optional<ArrayList<HasElement>> maybeChain = getPreservedChain(
event);
if (maybeChain.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());
} else {
// Create an empty chain which gets populated later in
// `createChainIfEmptyAndExecuteBeforeEnterNavigation`.
chain.clear();

// Has any preserved components already been created here? If so,
// we don't want to navigate back to them ever so clear cache for
// window.
clearAllPreservedChains(event.getUI());
}
return false;
}

/**
* Send before leave event to all listeners.
*
* @return optional return http status code
*/
private Optional<Integer> handleBeforeLeaveEvents(NavigationEvent event,
Class<? extends Component> routeTargetType,
RouteParameters parameters) {
BeforeLeaveEvent beforeNavigationDeactivating = new BeforeLeaveEvent(
event, routeTargetType, parameters, routeLayoutTypes);

return executeBeforeLeaveNavigation(event,
beforeNavigationDeactivating);
}

/**
* If a route refresh has been requested, remove all modal components. This
* is necessary because maintaining the correct modality cardinality and
* order is not feasible without knowing who opened them and when.
*
* @param event
* navigation event
*/
private static void cleanModalComponents(NavigationEvent event) {
if (event.getUI().hasModalComponent()
&& event.getTrigger() == NavigationTrigger.REFRESH_ROUTE) {
Component modalComponent;
while ((modalComponent = event.getUI().getInternals()
.getActiveModalComponent()) != null) {
modalComponent.removeFromParent();
}
}
}

/**
* Send before navigation event to all listeners.
*
* @return optional return http status code
*/
private Optional<Integer> handleBeforeNavigationEvents(
NavigationEvent event, Class<? extends Component> routeTargetType,
RouteParameters parameters, ArrayList<HasElement> chain) {
BeforeEnterEvent beforeNavigationActivating = new BeforeEnterEvent(
event, routeTargetType, parameters, routeLayoutTypes);

return createChainIfEmptyAndExecuteBeforeEnterNavigation(
beforeNavigationActivating, event, chain);
}

/**
* Send after navigation event to all listeners.
*
* @return optional return http status code
*/
private void handleAfterNavigationEvents(UI ui,
RouteParameters parameters) {
List<AfterNavigationHandler> afterNavigationHandlers = new ArrayList<>(
ui.getNavigationListeners(AfterNavigationHandler.class));
afterNavigationHandlers
Expand All @@ -284,10 +342,36 @@ public int handle(NavigationEvent event) {
fireAfterNavigationListeners(
new AfterNavigationEvent(locationChangeEvent, parameters),
afterNavigationHandlers);
}

updatePageTitle(event, componentInstance, route);
/**
* Check if target route is client handled and Flow should not handle
* rendering.
*
* @param route
* formatted route target string
* @return {@code true} if client handled render for route
*/
private boolean isClientHandled(String route) {
// If navigation target is Hilla route, terminate Flow navigation logic
// here.
return MenuRegistry.hasClientRoute(route, true)
&& !MenuRegistry.getClientRoutes(true).get(route).flowLayout();
}

return statusCode;
/**
* Get the target location as a standardized route string.
*
* @param event
* navigation event
* @return route string
*/
private static String getFormattedRoute(NavigationEvent event) {
return event.getLocation().getPath().isEmpty()
? event.getLocation().getPath()
: event.getLocation().getPath().startsWith("/")
? event.getLocation().getPath()
: "/" + event.getLocation().getPath();
}

/**
Expand Down Expand Up @@ -315,8 +399,7 @@ protected List<Class<? extends RouterLayout>> getTargetParentLayouts(
}

private void pushHistoryStateIfNeeded(NavigationEvent event, UI ui) {
if (event instanceof ErrorNavigationEvent) {
ErrorNavigationEvent errorEvent = (ErrorNavigationEvent) event;
if (event instanceof ErrorNavigationEvent errorEvent) {
if (isRouterLinkNotFoundNavigationError(errorEvent)) {
// #8544
event.getState().ifPresent(s -> ui.getPage().executeJs(
Expand Down Expand Up @@ -397,7 +480,11 @@ protected abstract void notifyNavigationTarget(Component componentInstance,
protected abstract List<Class<? extends RouterLayout>> getRouterLayoutTypes(
Class<? extends Component> routeTargetType, Router router);

// The last element in the returned list is always a Component class
/**
* Collect the element types chain for the current navigation state.
*
* @return types chain for navigation target
*/
private List<Class<? extends HasElement>> getTypesChain() {
final Class<? extends Component> routeTargetType = navigationState
.getNavigationTarget();
Expand All @@ -406,10 +493,10 @@ private List<Class<? extends HasElement>> getTypesChain() {
this.routeLayoutTypes);
Collections.reverse(layoutTypes);

final ArrayList<Class<? extends HasElement>> chain = new ArrayList<>();
for (Class<? extends RouterLayout> parentType : layoutTypes) {
chain.add(parentType);
}
final ArrayList<Class<? extends HasElement>> chain = new ArrayList<>(
layoutTypes);

// The last element in the returned list is always a Component class
chain.add(routeTargetType);
return chain;
}
Expand Down Expand Up @@ -496,7 +583,7 @@ private Deque<BeforeLeaveHandler> getBeforeLeaveHandlers(UI ui) {
/**
* Inform any {@link BeforeEnterObserver}s in attaching element chain. The
* event is sent first to the {@link BeforeEnterHandler}s registered within
* the {@link UI}, then to any element in the chain and to any of it's child
* the {@link UI}, then to any element in the chain and to any of its child
* components in the hierarchy which implements {@link BeforeEnterHandler}
*
* If the <code>chain</code> argument is empty <code>chainClasses</code> is
Expand All @@ -510,7 +597,7 @@ private Deque<BeforeLeaveHandler> getBeforeLeaveHandlers(UI ui) {
* @param chain
* the chain of {@link HasElement} instances which will be
* rendered. In case this is empty it'll be populated with
* instances according with the navigation event's location.
* instances according to the navigation event's location.
* @return result of observer events
*/
private Optional<Integer> createChainIfEmptyAndExecuteBeforeEnterNavigation(
Expand Down Expand Up @@ -614,7 +701,7 @@ private Optional<Integer> sendBeforeEnterEvent(

if (chain != null) {
// Reverse the chain to the stored ordered, since that is different
// than the notification order, and also to keep
// from the notification order, and also to keep
// LocationChangeEvent.getRouteTargetChain backward compatible.
chain = new ArrayList<>(chain);
Collections.reverse(chain);
Expand Down Expand Up @@ -692,8 +779,7 @@ private Optional<Integer> notifyNavigationTarget(NavigationEvent event,
*/
private static boolean isComponentElementEqualsOrChild(
BeforeEnterHandler eventHandler, Component component) {
if (eventHandler instanceof HasElement) {
HasElement hasElement = (HasElement) eventHandler;
if (eventHandler instanceof HasElement hasElement) {

final Element componentElement = component.getElement();

Expand Down Expand Up @@ -850,11 +936,11 @@ private NavigationEvent getNavigationEvent(NavigationEvent event,
/**
* Checks if there exists a cached component chain of the route location in
* the current window.
*
* <p>
* If retrieving the window name requires another round-trip, schedule it
* and make a new call to the handle {@link #handle(NavigationEvent)} in the
* callback. In this case, this method returns {@link Optional#empty()}.
*
* <p>
* If the chain is missing and needs to be created this method returns an
* {@link Optional} wrapping an empty {@link ArrayList}.
*/
Expand Down
Loading