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

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, inject } 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 {
  private pfeBusinessService = inject(PfeBusinessService);
  private pfeNavigationService = inject(PfeNavigationService);
  private pfeStateService = inject(PfeStateService);
  private pfeConfigService = inject(PfeConfigurationService);
  private pfeServiceActivatorService = inject(PfeServiceActivatorService);
  private pfeUtilService = inject(PfeUtilService);
  private pfeConditionsService = inject(PfeConditionsService);
  private cdr = inject(ChangeDetectorRef);
  private elementRef = inject(ElementRef);
  private router = inject(Router);

  // 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

  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$.pipe(takeUntil(this.unSubscribeSubject$)).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$.pipe(takeUntil(this.unSubscribeSubject$)).subscribe((status) => {
      this.pageStatus = status;
      this.cdr.detectChanges();
    });

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

    this.pfeBusinessService.errorMessage$.pipe(takeUntil(this.unSubscribeSubject$)).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();
  }
}
@if (headerTemplate) {
  <div>
    <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>
}

@if (errorMessageTemplate) {
  <div>
    <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>
}

@if (noConfigMessageTemplate) {
  <div>
    <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>
}

@if (pageOutlet) {
  <div>
    <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>
}

@if (navigationTemplate) {
  <div [@.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>
}

@if (footerTemplate) {
  <div>
    <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 ""