File

libs/ngx-pfe/services/pfe-routing/route-generator.service.ts

Index

Properties
Methods

Constructor

constructor(router: Router, pfeRouteGuardsService: PfeRouteGuardsService, pfePageMappingService: PfePageMappingService, pfeConfigService: PfeConfigurationService, pfeNavigationService: PfeNavigationService, pfeNavigationUtilService: PfeNavigationUtilService, pfeBusinessService: PfeBusinessService, pfePageRoutingEventsHandlerService: PFEPageRoutingEventsHandlerService, logger: NgxLoggerService)
Parameters :
Name Type Optional
router Router No
pfeRouteGuardsService PfeRouteGuardsService No
pfePageMappingService PfePageMappingService No
pfeConfigService PfeConfigurationService No
pfeNavigationService PfeNavigationService No
pfeNavigationUtilService PfeNavigationUtilService No
pfeBusinessService PfeBusinessService No
pfePageRoutingEventsHandlerService PFEPageRoutingEventsHandlerService No
logger NgxLoggerService No

Methods

Public generatePFERoutingConfig
generatePFERoutingConfig(pfeConfig: NgxPfeConfig)
Parameters :
Name Type Optional
pfeConfig NgxPfeConfig No
Returns : Route[]

Properties

Public canActivate
Type : CanActivateFn
Default value : () => {...}
Public router
Type : Router
import { NgxLoggerService } from '@allianz/ngx-logger';
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivateChildFn,
  CanActivateFn,
  CanDeactivateFn,
  CanLoadFn,
  CanMatchFn,
  Route,
  Router,
  RouterStateSnapshot,
} from '@angular/router';
import { PfeEmptyRoutingComponent } from '../../components/pfe-empty-routing/pfe-empty-routing.component';
import { AppConfiguration } from '../../models/app-configuration.model';
import { NavOptionConfig, isNavigationNode } from '../../models/navigation-config.model';
import { NgxPfeConfig, PageConfig } from '../../models/ngx-pfe-page-config.model';
import { PfeBusinessService } from '../pfe-business-service/business.service';
import { PfeConfigurationService } from '../pfe-config-service/config-service.service';
import { LAST_VISITED_PAGE_STATE_KEY, PfeNavigationUtilService } from '../pfe-navigation-service/navigation-util.service';
import { PfeNavigationService } from '../pfe-navigation-service/navigation.service';
import { PageDefinition } from '../pfe-page-mapping-service/page-definition';
import { PfePageMappingService } from '../pfe-page-mapping-service/page-mapping-service.service';
import { pfePageConfigResolver } from './page-config-resolver';
import { PFEPageRoutingEventsHandlerService } from './page-routing-events-handler.service';
import { PfeRouteGuardsService } from './route-generator-guards.service';
import { extractQueryParams } from './utils/extract-query-params';

@Injectable()
export class PFERouteGeneratorService {
  constructor(
    public router: Router,
    private pfeRouteGuardsService: PfeRouteGuardsService,
    private pfePageMappingService: PfePageMappingService,
    private pfeConfigService: PfeConfigurationService,
    private pfeNavigationService: PfeNavigationService,
    private pfeNavigationUtilService: PfeNavigationUtilService,
    private pfeBusinessService: PfeBusinessService,
    private pfePageRoutingEventsHandlerService: PFEPageRoutingEventsHandlerService,
    private logger: NgxLoggerService
  ) {}

  public generatePFERoutingConfig(pfeConfig: NgxPfeConfig): Route[] {
    const routerConfig: Route[] = [];

    if (pfeConfig.pagesConfiguration) {
      pfeConfig.pagesConfiguration.forEach((page) => {
        const pageDefinition: PageDefinition | undefined =
          this.pfePageMappingService.getPageDefinition(page) ?? this.getNavigationNodePageDefinition(pfeConfig, page);

        if (pageDefinition) {
          /**
           * The actual page route is added as a child to an empty parent route.
           * The background for this is, that Angular runs route guards of every "layer" in parallel.
           * That means, a configured route guard and the pfe route guard (PFEPageRoutingEventsHandlerService)
           * would run in parallel.
           *
           * This is not desired, as the pfe route guard could trigger service activators, etc...
           *
           * The solution is, to setup the configured route guards in a sequence that they run first.
           * This ensures, that they finish before the pfe guard. The pfe guard
           * will also not be executed when the parent returns a false result.
           *
           * See also: https://angular.io/guide/router-tutorial-toh#milestone-5-route-guards
           *
           * Execution order:
           * - From the deepest child towards the top: CanDeactivate, CanActivateChild
           * - From the top towards the children: CanActivate
           * - On module layer: CanMatch
           */
          const guards = this.pfeRouteGuardsService.generateGuards(page.guards);

          const route: Route = {
            path: '',
            canActivate: [...((guards?.canActivate as CanActivateFn[]) || [])],
            canDeactivate: [this.pfePageRoutingEventsHandlerService.canDeactivate],
            children: [],
          };

          const childRoute: Route = {
            path: page.pageId,
            canActivate: [this.pfePageRoutingEventsHandlerService.canActivate],
            canActivateChild: [...((guards?.canActivateChild as CanActivateChildFn[]) || [])],
            canDeactivate: [...((guards?.canDeactivate as CanDeactivateFn<unknown>[]) || [])],
            canMatch: [...((guards?.canMatch as CanMatchFn[]) || [])],
            canLoad: [...((guards?.canLoad as CanLoadFn[]) || [])],
            data: {
              pfePage: true,
            },
            resolve: {
              pageConfig: pfePageConfigResolver,
            },
          };

          if (pageDefinition.pageComponent) {
            childRoute.component = pageDefinition.pageComponent;
          } else {
            childRoute.loadChildren = pageDefinition?.lazyPageRoute?.loadChildren;
            childRoute.loadComponent = pageDefinition?.lazyPageRoute?.loadComponent;
          }

          route.children?.push(childRoute);

          routerConfig.push(route);
        } else {
          this.logger.errorToServer(`generatePFERoutingConfig: PageType used in configuration not found: `, page);
        }
      });
    }
    return routerConfig;
  }

  public canActivate: CanActivateFn = async (activatedRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> => {
    this.pfeNavigationService.navigationInProgress$.next(true);

    if (activatedRoute?.routeConfig?.children) {
      // Navigate to first child:
      // We can be sure that the structure below the activatedRoute looks like this, as we created it ourselves in the generatePFERoutingConfig()
      if (activatedRoute?.children?.[0]?.children?.length === 0) {
        const firstPageOption: NavOptionConfig | undefined = await this.pfeNavigationService.getFirstPageOrErrorPageOption();
        if (this.pfeNavigationUtilService.navigateExternal(firstPageOption)) {
          return false;
        } else if (firstPageOption?.nextPageId) {
          this.router.navigate([state.url, firstPageOption.nextPageId], { replaceUrl: true });
        }
        return false;
      }
      return true;
    }

    let config: NgxPfeConfig;
    try {
      config = await this.pfeConfigService.getConfig();
    } catch (error) {
      // Loading the config failed. Child routes cannot be generated. Master Page will trigger an error state to show an error message
      return true;
    }

    const pfeChildRoutes = this.generatePFERoutingConfig(config);
    if (activatedRoute?.parent?.routeConfig) {
      activatedRoute.parent.routeConfig.children = pfeChildRoutes;
      activatedRoute.parent.routeConfig.canActivate = [this.canActivate];
    }

    // Try to restore the state from server:
    let lastVisitedPage;
    try {
      await this.pfeBusinessService.restoreState();
      lastVisitedPage = this.pfeBusinessService.getFullState()[LAST_VISITED_PAGE_STATE_KEY];
    } catch (error) {
      const errorPageID = await this.pfeNavigationUtilService.getErrorPage(this.isHttpErrorResponse(error) ? error : undefined);
      if (errorPageID) {
        this.navigateToPageID(state, activatedRoute, errorPageID);
      } else {
        throw error;
      }
      return false;
    }

    // default is to navigate to the last page.
    let navigateToLastPage = true;
    if (activatedRoute.url[0] && activatedRoute.url[0].path) {
      const navConfig = await this.pfeConfigService.getPageNavigationConfiguration(activatedRoute.url[0].path);
      navigateToLastPage =
        navConfig !== undefined && navConfig.navigateToLastVisitedPage !== undefined ? navConfig.navigateToLastVisitedPage : true;
    }

    if (lastVisitedPage) {
      // We have a restored state:
      if (navigateToLastPage) {
        this.navigateToPageID(state, activatedRoute, lastVisitedPage);
      } else {
        // Retrigger navigation with current child:
        this.navigateWithURL(state.url);
      }
    } else if (
      (activatedRoute.url.length > 0 && (await this.getAllowGlobalDirectNavigation())) ||
      (await this.getAllowDirectNavigationForPage(activatedRoute))
    ) {
      // Directly navigated to child
      const pageID = activatedRoute.url[0].path;
      const pageConfig = await this.pfeConfigService.getPageConfiguration(pageID);

      if (!pageConfig) {
        // We don't have a config for that pageID, redirect to first page:
        const firstPageOption = await this.pfeNavigationService.getFirstPageOrErrorPageOption();
        if (this.pfeNavigationUtilService.navigateExternal(firstPageOption)) {
          return false;
        } else if (firstPageOption?.nextPageId) {
          this.navigateToChildOnSameLevel(state, activatedRoute, firstPageOption.nextPageId);
        }
      } else {
        // Retrigger navigation with current child:
        this.navigateWithURL(state.url);
      }
    } else {
      // Navigate to first child:
      const firstPageOption = await this.pfeNavigationService.getFirstPageOrErrorPageOption();
      if (this.pfeNavigationUtilService.navigateExternal(firstPageOption)) {
        return false;
      } else if (firstPageOption?.nextPageId) {
        this.navigateToPageID(state, activatedRoute, firstPageOption.nextPageId);
      }
    }
    return false;
  };

  private navigateToPageID(state: RouterStateSnapshot, activatedRoute: ActivatedRouteSnapshot, pageID: string) {
    if (activatedRoute.url.length > 0) {
      this.navigateToChildOnSameLevel(state, activatedRoute, pageID);
    } else {
      const { path, queryParams } = extractQueryParams(state.url);
      this.router.navigate([path, pageID], { replaceUrl: true, queryParams });
    }
  }

  private navigateToChildOnSameLevel(state: RouterStateSnapshot, activatedRoute: ActivatedRouteSnapshot, pageID: string) {
    const parentUrl = state.url.slice(0, state.url.indexOf(activatedRoute.url[activatedRoute.url.length - 1].path));
    this.router.navigate([parentUrl, pageID], { replaceUrl: true });
  }

  /**
   * Triggers a navigation, with a URL. Can handle query parameters correctly.
   *
   * We need to pay special attention to query parameters so we can deal with deep-linked
   * entry URLs. We can't simply use these URLs to navigate further because they might lead
   * to unidentifiable routes.
   * E.g. the router won't know how to deal with `my-page?key=value`. It's not a known route
   */
  private navigateWithURL(url: string) {
    const { path, queryParams } = extractQueryParams(url);
    this.router.navigate([path], { replaceUrl: true, queryParams });
  }

  private async getAllowGlobalDirectNavigation() {
    const appConfig: AppConfiguration | undefined = await this.pfeConfigService.getAppConfiguration();
    return appConfig?.pfeConfig?.allowDirectNavigationWithoutState;
  }

  /**
   * Determines if it is allowed to directly navigate to this specific navigation config.
   */
  private async getAllowDirectNavigationForPage(activatedRoute: ActivatedRouteSnapshot) {
    if (activatedRoute.url.length > 0) {
      // Directly navigated to child
      const pageID = activatedRoute.url[0].path;

      if (pageID) {
        const pageNavConfig = await this.pfeConfigService.getPageNavigationConfiguration(pageID);
        if (pageNavConfig && pageNavConfig.allowDirectNavigationWithoutState) {
          return true;
        }
      }
    }
    return false;
  }

  private getNavigationNodePageDefinition(pfeConfig: NgxPfeConfig, page: PageConfig<string>): PageDefinition | undefined {
    for (const pageNavConfig of pfeConfig.navConfiguration.pages) {
      if (page.pageId === pageNavConfig.pageId && isNavigationNode(pageNavConfig)) {
        return new PageDefinition(PfeEmptyRoutingComponent);
      }
    }
  }

  private isHttpErrorResponse(error: HttpErrorResponse | unknown): error is HttpErrorResponse {
    return error instanceof HttpErrorResponse;
  }
}

results matching ""

    No results matching ""