Skip to content

Commit

Permalink
chore: refactr navigation state renderer (#20777)
Browse files Browse the repository at this point in the history
Make the handle workflow
easier to read.
Add javadoc for clarification
of parts that are executed.
  • Loading branch information
caalador authored Jan 3, 2025
1 parent d7258e0 commit e365417
Showing 1 changed file with 158 additions and 72 deletions.
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

0 comments on commit e365417

Please sign in to comment.