import { NgxLoggerService } from '@allianz/ngx-logger';
import { Location } from '@angular/common';
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, CanDeactivateFn, Router, RouterStateSnapshot } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { NavOptionConfig, NavigationType, PageNavigationConfiguration, isNavigationNode } from '../../models/navigation-config.model';
import { PfeServiceActivatorService } from '../../pfe-service-activator/service/service-activator.service';
import { PfeConfigurationService } from '../pfe-config-service/config-service.service';
import { PfeModuleConfigurationService } from '../pfe-module-configuration/pfe-module-configuration.service';
import { LAST_VISITED_PAGE_STATE_KEY, PfeNavigationUtilService } from '../pfe-navigation-service/navigation-util.service';
import { PfeNavigationService } from '../pfe-navigation-service/navigation.service';
import { AngularNavigationExtrasPFE } from '../pfe-navigation-service/pfe-navigation-extras';
import { PFE_STATE_NAVIGATING_FROM, PFE_STATE_NAVIGATING_TO } from '../pfe-state-service/pfe-state-keys';
import { PfeStateService } from '../pfe-state-service/state.service';
import { PfeActionsService } from './../../pfe-actions/pfe-actions.service';
import { BYPASS_PFE_ROUTING } from './route-generator-guards.definition';
import { getPageIDFromRouteSnapshot } from './utils/page-id-from-route-snapshot';
@Injectable()
export class PFEPageRoutingEventsHandlerService {
public browserBackButtonPressed$: BehaviorSubject<string | undefined> = new BehaviorSubject<string | undefined>(undefined);
private pfeModuleConfigurationService = inject(PfeModuleConfigurationService);
constructor(
public router: Router,
private pfeServiceActivatorService: PfeServiceActivatorService,
private pfeConfigService: PfeConfigurationService,
private pfeNavigationService: PfeNavigationService,
private pfeNavigationUtilService: PfeNavigationUtilService,
private location: Location,
private pfeStateService: PfeStateService,
private pfeActionsService: PfeActionsService,
private logger: NgxLoggerService
) {
this.location.subscribe((event) => {
if (event.url) {
const nextPage = event.url.split('/').pop();
if (nextPage && event.type === 'popstate') {
this.browserBackButtonPressed$.next(nextPage);
}
}
});
}
public canActivate: CanActivateFn = async (activatedRoute: ActivatedRouteSnapshot): Promise<boolean> => {
if (this.router.getCurrentNavigation()?.extras?.state?.[BYPASS_PFE_ROUTING]) {
return true;
}
const nextPageNavConfig: PageNavigationConfiguration | undefined = await this.pfeConfigService.getPageNavigationConfiguration(
activatedRoute?.routeConfig?.path
);
// Set "PFE_STATE_NAVIGATING_TO" for the first page (for other pages it is set via canDeactivate when a page is left)
if (!this.pfeStateService.getStateValueByExpression(PFE_STATE_NAVIGATING_TO)) {
this.pfeStateService.storeValueByExpression(PFE_STATE_NAVIGATING_TO, activatedRoute?.routeConfig?.path);
}
let success = true;
if (nextPageNavConfig) {
try {
if (nextPageNavConfig.onPageEnterActions) {
const actionsResult = await this.pfeActionsService.executeActions(nextPageNavConfig.onPageEnterActions);
if (!actionsResult) {
await this.handleNavigationEnd(false);
return false;
}
}
} catch (error) {
// One of the actions failed
success = false;
}
try {
if (nextPageNavConfig.onPageEnterServiceActivators) {
await this.pfeServiceActivatorService.spreadWithGlobalServiceActivatorConfig(nextPageNavConfig.onPageEnterServiceActivators);
await this.pfeServiceActivatorService.handleServiceActivators(
nextPageNavConfig.onPageEnterServiceActivators,
nextPageNavConfig.executeServiceActivatorsSync
);
}
} catch (error) {
// Synchronous service activator call failed:
success = false;
}
try {
await this.pfeServiceActivatorService.waitForAsyncServiceActivators(nextPageNavConfig.pageId);
} catch (error) {
success = false;
}
}
// Update state values from the page config
if (nextPageNavConfig?.pageId) {
const config = await this.pfeConfigService.getPageConfiguration(nextPageNavConfig.pageId);
if (config?.updateStateValues) {
await this.pfeStateService.updateStateValues(config.updateStateValues);
}
}
if (nextPageNavConfig) {
if (!nextPageNavConfig.omitFromHistory && !isNavigationNode(nextPageNavConfig)) {
this.pfeStateService.storeValue(LAST_VISITED_PAGE_STATE_KEY, nextPageNavConfig.pageId);
}
if (this.pfeModuleConfigurationService.pfeModuleConfig().enableAutomaticStateStorage) {
this.pfeNavigationService.updateStoredState(nextPageNavConfig);
}
}
// If the page is a navigationNode, another navigation is triggered immediately and the page is never entered.
if (isNavigationNode(nextPageNavConfig)) {
const navigationResult = await this.pfeNavigationService.navigateNextWithConfig({ ...nextPageNavConfig, omitFromHistory: true });
if (!navigationResult) {
this.pfeNavigationService.navigationInProgress$.next(false);
}
return false;
}
// These events were handed with Angular router events in the past
// But that causes issues with secondary router outlets
// Handling them here is safe in all situations
await this.handleNavigationEnd(success);
return success;
};
/**
* Determines if a back or forward navigation was triggered by the browser buttons.
* If this is the case a pfe navigation is triggered.
*/
public canDeactivate: CanDeactivateFn<unknown> = async (
component: unknown,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot
): Promise<boolean> => {
if (this.router.getCurrentNavigation()?.extras?.state?.[BYPASS_PFE_ROUTING]) {
return true;
}
this.handleNavigationStartBegin();
let allowNavigation = true;
let navigationType = this.router.getCurrentNavigation()?.extras?.state?.navigationType;
if (this.browserBackButtonPressed$.value) {
const nextPageFromBrowserButton = this.browserBackButtonPressed$.value;
this.browserBackButtonPressed$.next(undefined);
const browserButtonNavigationResult = await this.handleUserPressedBrowserButtonNavigation(nextPageFromBrowserButton);
allowNavigation = browserButtonNavigationResult.allowNavigation;
navigationType = browserButtonNavigationResult.navigationType;
}
if (allowNavigation && nextState) {
allowNavigation = await this.runOnNavigationStartActions(nextState);
if (allowNavigation) {
await this.handlePageHistory(nextState, navigationType);
}
}
const currentNavigationPFEExtras = this.router.getCurrentNavigation()?.extras as AngularNavigationExtrasPFE;
if (!allowNavigation) {
this.handleNavigationStartCancel();
return this.preventNavigation(currentState);
// If a navigation has the error page as target, the navigation itself is successful,
// but things like cleaning up the error message from the state should still not happen.
} else if (!currentNavigationPFEExtras?.state?.pfe?.navigatingToErrorPage) {
await this.handleNavigationStartSuccess();
}
return allowNavigation;
};
private async handlePageHistory(nextState: RouterStateSnapshot, navigationType: NavigationType) {
const currentPageNavConfig: PageNavigationConfiguration | undefined = await this.pfeConfigService.getPageNavigationConfiguration(
this.pfeNavigationService.currentPageId$.value
);
let shouldPopPageFromHistory = false;
if (navigationType === NavigationType.BACKWARD) {
shouldPopPageFromHistory = true;
}
if (!navigationType) {
const nextPreviousPage = this.pfeNavigationService.getPreviousPageOnHistory();
shouldPopPageFromHistory = nextPreviousPage === getPageIDFromRouteSnapshot(nextState.root);
}
if (shouldPopPageFromHistory) {
this.pfeNavigationService.popPageFromHistory(currentPageNavConfig?.omitFromHistory);
}
}
private async handleUserPressedBrowserButtonNavigation(
nextPageFromBrowserButton: string
): Promise<{ allowNavigation: boolean; navigationType?: NavigationType }> {
const currentPageID = this.pfeNavigationService.currentPageId$.value;
const currentPageNavConfig: PageNavigationConfiguration | undefined =
await this.pfeConfigService.getPageNavigationConfiguration(currentPageID);
if (!currentPageNavConfig) {
// There is no navigation config, we don't know what to do...
this.logger.warnToServer(`No navigation config for page: ${currentPageID}`);
}
let nextPageOption: NavOptionConfig | undefined;
let nextPreviousPageOption: NavOptionConfig | undefined;
if (currentPageNavConfig?.nextOptionList) {
nextPageOption = await this.pfeNavigationService.getNextPageOption(currentPageNavConfig);
}
if (currentPageNavConfig?.backOptionList) {
nextPreviousPageOption = await this.pfeNavigationService.getPreviousPageOption(currentPageNavConfig);
}
if (nextPageOption?.nextPageId === nextPageFromBrowserButton) {
if (this.pfeNavigationUtilService.navigateExternal(nextPageOption)) {
return { allowNavigation: false };
}
// User was navigating forward in the flow by either the browser button or a url change
// Prevent the navigation forward if the page status is false:
if (!this.pfeNavigationService.pageStatus$.getValue()) {
return { allowNavigation: false };
} else {
const navigationResult = await this.pfeNavigationService.navigateNext(true);
if (!navigationResult) {
return { allowNavigation: false };
}
return { allowNavigation: true, navigationType: NavigationType.FORWARD };
}
}
if (nextPreviousPageOption?.nextPageId === nextPageFromBrowserButton) {
if (this.pfeNavigationUtilService.navigateExternal(nextPreviousPageOption)) {
return { allowNavigation: false };
}
}
const nextPreviousPage = await this.getNextPreviousPage(currentPageNavConfig);
if (nextPreviousPage === nextPageFromBrowserButton) {
// User was navigating backward in the flow by either the browser button or a url change
const navigationResult = await this.pfeNavigationService.navigateBack(true);
if (!navigationResult) {
return { allowNavigation: false };
}
return { allowNavigation: true, navigationType: NavigationType.BACKWARD };
}
// We were unable to determine, if the user navigated back or forward, prevent the navigation
return { allowNavigation: false };
}
private async getNextPreviousPage(currentPageNavConfig?: PageNavigationConfiguration): Promise<string | undefined> {
let nextPreviousPage: string | undefined;
if (currentPageNavConfig && currentPageNavConfig.backOptionList) {
const nextPreviousPageOption = await this.pfeNavigationService.getPreviousPageOption(currentPageNavConfig);
nextPreviousPage = nextPreviousPageOption?.nextPageId;
} else {
nextPreviousPage = this.pfeNavigationService.getPreviousPageOnHistory(currentPageNavConfig?.omitFromHistory);
}
return nextPreviousPage;
}
private preventNavigation(currentState: RouterStateSnapshot): boolean {
// There is an angular bug that requires the workaround to push the current url into the browser history in that case
// References:
// https://github.com/angular/angular/issues/13586
// https://github.com/angular/angular/pull/18135
// https://github.com/angular/angular/issues/15664
// Push the current page into the history:
this.location.go(currentState.url);
return false;
}
private async runOnNavigationStartActions(nextState: RouterStateSnapshot): Promise<boolean> {
this.setNavigationState(nextState);
const currentPageID = this.pfeNavigationService.currentPageId$.value;
const currentPageNavConfig: PageNavigationConfiguration | undefined =
await this.pfeConfigService.getPageNavigationConfiguration(currentPageID);
if (currentPageNavConfig) {
try {
if (currentPageNavConfig.onNavigationStartActions) {
const actionsResult = await this.pfeActionsService.executeActions(currentPageNavConfig.onNavigationStartActions, (notBusy) =>
this.pfeNavigationService.navigationInProgress$.next(!notBusy)
);
if (!actionsResult) {
return false;
}
}
} catch (error) {
// One of the actions failed
return false;
}
}
return true;
}
/**
* Set the navigationState within the state.
* This contains the page that is being navigated to and the
* one where the navigation is coming from.
*/
private setNavigationState(nextState: RouterStateSnapshot) {
this.pfeStateService.storeValueByExpression(PFE_STATE_NAVIGATING_FROM, this.pfeNavigationService.currentPageId$.value);
// The RouterStateSnapshot contains the router state starting from the root/top level
// The top level gives us the reference to the active children and with that the whole tree.
// We need to search the children, until we find the pfe routing page
const nextPageID = getPageIDFromRouteSnapshot(nextState.root);
if (nextPageID) {
this.pfeStateService.storeValueByExpression(PFE_STATE_NAVIGATING_TO, nextPageID);
}
}
/**
* If needed, it would be possible to expose these "events" in a similar fashion like the Angular Router
*/
/**
* Handles everything related to the ending of a navigation.
* If the navigation was not successful (canceled in the last step), the
* pageStatus$ and error message are not reset, as it is likely tha the user tries it again.
*/
private async handleNavigationEnd(success: boolean) {
if (success) {
// Reset the page status after the navigation, but before the page itself becomes active:
this.pfeNavigationService.pageStatus$.next(false);
}
this.pfeNavigationService.navigationInProgress$.next(false);
}
/**
* Handles a cancellation of the navigation at any point.
*/
private handleNavigationStartCancel() {
this.pfeNavigationService.navigationInProgress$.next(false);
}
/**
* Handles the start of a new navigation
*/
private handleNavigationStartBegin() {
this.pfeNavigationService.navigationInProgress$.next(true);
}
/**
* Runs after the handleNavigationStartBegin and the navigation handling in the guard
*/
private async handleNavigationStartSuccess() {
this.pfeStateService.storeValue(await this.pfeConfigService.getErrorMessageKey(), undefined);
}
}