File

libs/ngx-pfe/components/pfe-master-page/master-page.component.ts

Description

The master page is the central PFE component. All pages are displayed within this component.

Implements

OnInit OnDestroy

Metadata

Index

Properties
Methods
Inputs

Constructor

constructor(pfeBusinessService: PfeBusinessService, pfeNavigationService: PfeNavigationService, pfeStateService: PfeStateService, pfeConfigService: PfeConfigurationService, pfeServiceActivatorService: PfeServiceActivatorService, pfeUtilService: PfeUtilService, pfeConditionsService: PfeConditionsService, cdr: ChangeDetectorRef, elementRef: ElementRef, router: Router)
Parameters :
Name Type Optional
pfeBusinessService PfeBusinessService No
pfeNavigationService PfeNavigationService No
pfeStateService PfeStateService No
pfeConfigService PfeConfigurationService No
pfeServiceActivatorService PfeServiceActivatorService No
pfeUtilService PfeUtilService No
pfeConditionsService PfeConditionsService No
cdr ChangeDetectorRef No
elementRef ElementRef No
router Router No

Inputs

nextClickedCallback
Type : function

Methods

Async backClicked
backClicked()
Returns : any
Async nextClicked
nextClicked(nextClickedCallback: () => void)
Parameters :
Name Type Optional
nextClickedCallback function No
Returns : any

Properties

appConfiguration
Type : AppConfiguration | undefined
Public backButtonLabel
Type : string
Public errorMessage
Type : string | undefined
Public errorMessageTemplate
Type : TemplateRef<> | null
Default value : null
Decorators :
@ContentChild('errorMessageTemplate', {read: TemplateRef, static: true})
Public footerTemplate
Type : TemplateRef<> | null
Default value : null
Decorators :
@ContentChild('footerTemplate', {read: TemplateRef, static: true})
Public headerTemplate
Type : TemplateRef<> | null
Default value : null
Decorators :
@ContentChild('headerTemplate', {read: TemplateRef, static: true})
Public labelCondition
Default value : false
Public navigateBackCallback
Type : function
Public navigateCallback
Type : function
Public navigateNextCallback
Type : function
Public navigationOrServiceActivatorInProgress
Default value : false

Flag, that is set to true, while a navigation or service activator call is active.

Public navigationTemplate
Type : TemplateRef<> | null
Default value : null
Decorators :
@ContentChild('navigationTemplate', {read: TemplateRef, static: true})

References to templates passed as ng-content

Public nextButtonLabel
Type : string
Public noConfig
Default value : false
Public noConfigMessageTemplate
Type : TemplateRef<> | null
Default value : null
Decorators :
@ContentChild('noConfigMessageTemplate', {read: TemplateRef, static: true})
pageConfig
Type : PageConfig | undefined
Public pageOutlet
Type : TemplateRef<> | null
Default value : null
Decorators :
@ContentChild('pageOutlet', {read: TemplateRef, static: true})
Public pageStatus
Default value : false

The pageStatus determines if the page is valid. It is used, to prevent navigation in certain cases. For example, when a form is not filled out completely.

Public subscriptions
Type : Subscription
import { ChangeDetectorRef, Component, ContentChild, ElementRef, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Router } from '@angular/router';
import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { fadeIn } from '../../animations/animations';
import { AppConfiguration, PfeConfig } from '../../models/app-configuration.model';
import { PageConfig } from '../../models/ngx-pfe-page-config.model';
import { PfeConditionsService, PfeRulesEvaluator } from '../../pfe-conditions/public-api';
import { PfeServiceActivatorService } from '../../pfe-service-activator/service/service-activator.service';
import { PfeUtilService } from '../../pfe-util/services/util.service';
import { PfeBusinessService } from '../../services/pfe-business-service/business.service';
import { PfeConfigurationService } from '../../services/pfe-config-service/config-service.service';
import { PfeNavigationService } from '../../services/pfe-navigation-service/navigation.service';
import { PfeStateService } from '../../services/pfe-state-service/state.service';

/**
 * The master page is the central PFE component.
 * All pages are displayed within this component.
 *
 * @export
 */
@Component({
  selector: 'pfe-master-page',
  templateUrl: 'master-page.component.html',
  styleUrls: ['./master-page.component.scss'],
  animations: [fadeIn],
  standalone: false,
})
export class PfeMasterPageComponent implements OnInit, OnDestroy {
  // Unfortunately, the template context has to be provided in the template as separate objects.
  /**
   * References to templates passed as ng-content
   */
  @ContentChild('navigationTemplate', { read: TemplateRef, static: true })
  public navigationTemplate: TemplateRef<unknown> | null = null;

  @ContentChild('errorMessageTemplate', { read: TemplateRef, static: true })
  public errorMessageTemplate: TemplateRef<unknown> | null = null;

  @ContentChild('noConfigMessageTemplate', { read: TemplateRef, static: true })
  public noConfigMessageTemplate: TemplateRef<unknown> | null = null;

  @ContentChild('pageOutlet', { read: TemplateRef, static: true })
  public pageOutlet: TemplateRef<unknown> | null = null;

  @ContentChild('headerTemplate', { read: TemplateRef, static: true })
  public headerTemplate: TemplateRef<unknown> | null = null;

  @ContentChild('footerTemplate', { read: TemplateRef, static: true })
  public footerTemplate: TemplateRef<unknown> | null = null;

  @Input() nextClickedCallback!: () => Promise<boolean>;

  // Only to be accessed by the template:
  pageConfig: PageConfig | undefined;
  appConfiguration!: AppConfiguration | undefined;

  public navigateCallback!: (nextPage: string) => void;
  public navigateNextCallback!: () => void;
  public navigateBackCallback!: () => void;

  public nextButtonLabel!: string;
  public backButtonLabel!: string;

  /**
   * The pageStatus determines if the page is valid.
   * It is used, to prevent navigation in certain cases.
   * For example, when a form is not filled out completely.
   */
  public pageStatus = false;

  // TODO: Add proper no config error handling:
  public noConfig = false;

  public subscriptions!: Subscription;

  /**
   * Flag, that is set to true, while a navigation or service activator call is active.
   */
  public navigationOrServiceActivatorInProgress = false;
  public labelCondition = false;
  public errorMessage: string | undefined;
  private errorState = false;

  private componentDestroyed$ = new Subject<void>();
  private unSubscribeSubject$ = new Subject<void>();

  // TODO: add template (=could be xtra error page with hardcoded config) and default for generic error page, that works without config

  constructor(
    private pfeBusinessService: PfeBusinessService,
    private pfeNavigationService: PfeNavigationService,
    private pfeStateService: PfeStateService,
    private pfeConfigService: PfeConfigurationService,
    private pfeServiceActivatorService: PfeServiceActivatorService,
    private pfeUtilService: PfeUtilService,
    private pfeConditionsService: PfeConditionsService,
    private cdr: ChangeDetectorRef,
    private elementRef: ElementRef,
    private router: Router
  ) {}

  public ngOnInit() {
    this.navigateCallback = this.navigate.bind(this);
    this.navigateNextCallback = this.nextClicked.bind(this, this.nextClickedCallback);
    this.navigateBackCallback = this.backClicked.bind(this);

    this.pfeNavigationService.currentPageId$.subscribe((pageID) => {
      (async () => {
        this.pageConfig = await this.pfeConfigService.getPageConfiguration(pageID);
        if (this.pageConfig) {
          this.updatePageStatus(this.pageConfig);
        }
      })();
    });

    // Keep the new pageStatus subject state in sync for the master page:
    this.pfeNavigationService.pageStatus$.subscribe((status) => {
      this.pageStatus = status;
      this.cdr.detectChanges();
    });

    this.pfeBusinessService.busy$.subscribe((busy) => {
      this.navigationOrServiceActivatorInProgress = busy;
    });

    this.pfeBusinessService.errorMessage$.subscribe((message) => {
      this.errorMessage = message;
      setTimeout(() => {
        this.elementRef?.nativeElement?.scrollIntoView?.({ block: 'start', behavior: 'smooth' });
      });
    });

    (async () => {
      try {
        if (!(await this.pfeBusinessService.hasConfig())) {
          this.noConfig = true;
          this.errorState = true;
          // TODO: Show generic error page, that works without configuration, should be done in config service.
          // this.currentPageID = await this.pfeBusinessService.getErrorPage();
        }

        this.appConfiguration = await this.pfeConfigService.getAppConfiguration();
        this.initializeMasterPageAppConfiguration(this.appConfiguration?.pfeConfig);
      } catch (error) {
        this.noConfig = true;
        this.errorState = true;
        throw error;
      }
    })();
  }

  async nextClicked(nextClickedCallback: () => Promise<boolean>) {
    if (nextClickedCallback) {
      const canNavigate = await nextClickedCallback();

      if (canNavigate) {
        this.nextClickedNavigate();
      }
    } else {
      this.nextClickedNavigate();
    }
  }

  async backClicked() {
    this.pfeUtilService.trackingEvents$.next({ event: 'navigationButton', value: 'BACK_BUTTON' });
    if (this.errorState) {
      return;
    }

    await this.pfeNavigationService.navigateBack();
  }

  public ngOnDestroy() {
    this.unSubscribeSubject$.next();
    this.unSubscribeSubject$.complete();
    this.componentDestroyed$.next();
    this.componentDestroyed$.complete();
  }

  private checkChangeTextButton(pageConfig: PageConfig) {
    this.labelCondition = false;
    const btnCondition = pageConfig.nextBtnOptionalLabelCondition;

    if (btnCondition) {
      const propertyKeys = PfeRulesEvaluator.getStatePropertyName(btnCondition);
      propertyKeys.forEach((key) => {
        this.pfeBusinessService
          .getObservableForKey(key)
          .pipe(takeUntil(this.unSubscribeSubject$))
          .subscribe(() => {
            const result = this.pfeConditionsService.evaluateConditions(btnCondition, this.pfeStateService.getFullState());
            if (result !== this.labelCondition) {
              this.labelCondition = result;
              this.cdr.detectChanges();
            }
          });
      });
    }
  }

  private initializeMasterPageAppConfiguration(pfeConfig: PfeConfig | undefined) {
    if (pfeConfig) {
      this.nextButtonLabel = pfeConfig.nextButtonLabel ?? 'next_button';
      this.backButtonLabel = pfeConfig.backButtonLabel ?? 'back_button';
    }
  }

  /**
   * Triggered by changes in the navigation.
   */
  private updatePageStatus(pageConfig: PageConfig) {
    this.unSubscribeSubject$.next();
    this.checkChangeTextButton(pageConfig);
  }

  private async navigate(nextPage: string) {
    await this.pfeNavigationService.navigate(nextPage);
  }

  private async nextClickedNavigate() {
    this.pfeUtilService.trackingEvents$.next({ event: 'navigationButton', value: 'NEXT_BUTTON' });
    if (this.errorState) {
      return;
    }

    await this.pfeNavigationService.navigateNext();
  }
}
<div *ngIf="headerTemplate">
  <ng-container
    *ngTemplateOutlet="
      headerTemplate;
      context: {
        appConfiguration: appConfiguration,
        pageConfig: pageConfig,
        nextButtonLabel: nextButtonLabel,
        backButtonLabel: backButtonLabel,
        navigate: navigateCallback,
        navigateNext: navigateNextCallback,
        navigateBack: navigateBackCallback,
        labelCondition: labelCondition,
        pageStatus: pageStatus,
        errorMessage: errorMessage,
        noConfig: noConfig,
        navigationOrServiceActivatorInProgress: navigationOrServiceActivatorInProgress
      }
    "
  ></ng-container>
</div>

<div *ngIf="errorMessageTemplate">
  <ng-container
    *ngTemplateOutlet="
      errorMessageTemplate;
      context: {
        appConfiguration: appConfiguration,
        pageConfig: pageConfig,
        nextButtonLabel: nextButtonLabel,
        backButtonLabel: backButtonLabel,
        navigate: navigateCallback,
        navigateNext: navigateNextCallback,
        navigateBack: navigateBackCallback,
        labelCondition: labelCondition,
        pageStatus: pageStatus,
        errorMessage: errorMessage,
        noConfig: noConfig,
        navigationOrServiceActivatorInProgress: navigationOrServiceActivatorInProgress
      }
    "
  ></ng-container>
</div>

<div *ngIf="noConfigMessageTemplate">
  <ng-container
    *ngTemplateOutlet="
      noConfigMessageTemplate;
      context: {
        appConfiguration: appConfiguration,
        pageConfig: pageConfig,
        nextButtonLabel: nextButtonLabel,
        backButtonLabel: backButtonLabel,
        navigate: navigateCallback,
        navigateNext: navigateNextCallback,
        navigateBack: navigateBackCallback,
        labelCondition: labelCondition,
        pageStatus: pageStatus,
        errorMessage: errorMessage,
        noConfig: noConfig,
        navigationOrServiceActivatorInProgress: navigationOrServiceActivatorInProgress
      }
    "
  ></ng-container>
</div>

<div *ngIf="pageOutlet">
  <ng-container
    *ngTemplateOutlet="
      pageOutlet;
      context: {
        appConfiguration: appConfiguration,
        pageConfig: pageConfig,
        nextButtonLabel: nextButtonLabel,
        backButtonLabel: backButtonLabel,
        navigate: navigateCallback,
        navigateNext: navigateNextCallback,
        navigateBack: navigateBackCallback,
        labelCondition: labelCondition,
        pageStatus: pageStatus,
        errorMessage: errorMessage,
        noConfig: noConfig,
        navigationOrServiceActivatorInProgress: navigationOrServiceActivatorInProgress
      }
    "
  ></ng-container>
</div>

<div *ngIf="navigationTemplate" [@.disabled]="!!pageConfig?.disableAnimations" [@routeAnimations]="pageConfig?.pageId">
  <ng-container
    *ngTemplateOutlet="
      navigationTemplate;
      context: {
        appConfiguration: appConfiguration,
        pageConfig: pageConfig,
        nextButtonLabel: nextButtonLabel,
        backButtonLabel: backButtonLabel,
        navigate: navigateCallback,
        navigateNext: navigateNextCallback,
        navigateBack: navigateBackCallback,
        labelCondition: labelCondition,
        pageStatus: pageStatus,
        errorMessage: errorMessage,
        noConfig: noConfig,
        navigationOrServiceActivatorInProgress: navigationOrServiceActivatorInProgress
      }
    "
  ></ng-container>
</div>

<div *ngIf="footerTemplate">
  <ng-container
    *ngTemplateOutlet="
      footerTemplate;
      context: {
        appConfiguration: appConfiguration,
        pageConfig: pageConfig,
        nextButtonLabel: nextButtonLabel,
        backButtonLabel: backButtonLabel,
        navigate: navigateCallback,
        navigateNext: navigateNextCallback,
        navigateBack: navigateBackCallback,
        labelCondition: labelCondition,
        pageStatus: pageStatus,
        errorMessage: errorMessage,
        noConfig: noConfig,
        navigationOrServiceActivatorInProgress: navigationOrServiceActivatorInProgress
      }
    "
  ></ng-container>
</div>

./master-page.component.scss

.master-page {
  padding-bottom: 20px;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""