File

libs/ngx-pfe/services/pfe-routing/page-routing-events-handler.service.ts

Index

Properties

Constructor

constructor(router: Router, pfeServiceActivatorService: PfeServiceActivatorService, pfeConfigService: PfeConfigurationService, pfeNavigationService: PfeNavigationService, pfeNavigationUtilService: PfeNavigationUtilService, location: Location, pfeStateService: PfeStateService, pfeActionsService: PfeActionsService, logger: NgxLoggerService)
Parameters :
Name Type Optional
router Router No
pfeServiceActivatorService PfeServiceActivatorService No
pfeConfigService PfeConfigurationService No
pfeNavigationService PfeNavigationService No
pfeNavigationUtilService PfeNavigationUtilService No
location Location No
pfeStateService PfeStateService No
pfeActionsService PfeActionsService No
logger NgxLoggerService No

Properties

Public browserBackButtonPressed$
Type : BehaviorSubject<string | undefined>
Default value : new BehaviorSubject<string | undefined>(undefined)
Public canActivate
Type : CanActivateFn
Default value : () => {...}
Public canDeactivate
Type : CanDeactivateFn<>
Default value : () => {...}

Determines if a back or forward navigation was triggered by the browser buttons. If this is the case a pfe navigation is triggered.

Public router
Type : Router
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);
  }
}

results matching ""

    No results matching ""