File

libs/pfe-connector/src/lib/pfe-facade.ts

Description

The PFE Facades is our main binding between the PFE State and our Building Blocks. A) State is saved into the PFE whenever it's changed B) State is restored from PFE only upon connect. This means if the PFE changes the store value of our Building Block while already connected our Building Block will not get that data delivered. This is currently expected behavior as we really don't won't the PFE to interfere with our Building Block outside of our defined lifecycle of connect/update/disconnect.

Extends

AbstractBuildingBlockFacade

Index

Properties
Methods

Constructor

constructor(buildingBlock: BuildingBlockInterface, _pfeBusinessService: PfeBusinessService, buildingBlockMetaService: BuildingBlockMetaService, frameNavigationService: TalyFrameNavigationService, pfeResourcesService: TalyResourcesService, businessEventService: TalyBusinessEventService, aclService: AclService, pfeTrackingService: PfeTrackingService | null)
Parameters :
Name Type Optional
buildingBlock BuildingBlockInterface No
_pfeBusinessService PfeBusinessService No
buildingBlockMetaService BuildingBlockMetaService No
frameNavigationService TalyFrameNavigationService No
pfeResourcesService TalyResourcesService No
businessEventService TalyBusinessEventService No
aclService AclService No
pfeTrackingService PfeTrackingService | null No

Properties

stateChangesObservable
Type : Observable<>
_isConnected
Default value : false
connected$
Default value : new Subject<void>()
disconnected$
Default value : new Subject<void>()

Methods

connect
connect()
Returns : void
disconnect
disconnect()
Returns : void
Static create
create(block: BuildingBlockInterface)
Parameters :
Name Type Optional
block BuildingBlockInterface No
markFormGroupAsTouched
markFormGroupAsTouched()
Returns : void
import { PfeBusinessService } from '@allianz/ngx-pfe';
import { PfeTrackingService } from '@allianz/ngx-pfe/tracking';
import { AclService } from '@allianz/taly-acl/angular';
import { TalyFrameNavigationService } from '@allianz/taly-common/frame';
import {
  AbstractBuildingBlockFacade,
  BUILDING_BLOCK_NAVIGATION_TYPE,
  BuildingBlockInterface,
  BuildingBlockMetaService,
  BuildingBlockNavigationEvent,
  BusinessEvent,
  TalyBusinessEventService,
  TalyResourcesService,
  ValidationRule,
  isImplicitNavigation
} from '@allianz/taly-core';
import { trackForm } from '@allianz/taly-core/form-tracking';
import { Observable } from 'rxjs';
import { filter, take, takeUntil, tap } from 'rxjs/operators';
import { BuildingBlockResourceMap, StaticResourceList } from './types';
import * as pfeResources from './utils/pfe-resources';
import { replacePFEQueryWithStream } from './utils/pfe-validator';
import { tapOnce } from './utils/tap-once-operators';

/**
 * The PFE Facades is our main binding between the PFE State and our Building Blocks.
 * A) State is saved into the PFE whenever it's changed
 * B) State is restored from PFE only upon connect. This means if the PFE
 * changes the store value of our Building Block while already connected our Building Block will not get that data delivered.
 * This is currently expected behavior as we really don't won't the PFE to interfere with our Building Block
 * outside of our defined lifecycle of connect/update/disconnect.
 */
export class PFEFacade extends AbstractBuildingBlockFacade {
  stateChangesObservable!: Observable<unknown>;

  private pfeBackupStorageId = `$._taly.bb_backups['${this.buildingBlock.id}']`;
  private pfeStorageId = this.buildingBlock.id;

  constructor(
    buildingBlock: BuildingBlockInterface,
    private _pfeBusinessService: PfeBusinessService,
    private buildingBlockMetaService: BuildingBlockMetaService,
    private frameNavigationService: TalyFrameNavigationService,
    private pfeResourcesService: TalyResourcesService,
    private businessEventService: TalyBusinessEventService,
    private aclService: AclService,
    private pfeTrackingService: PfeTrackingService | null
  ) {
    super(buildingBlock);
    this.init();
  }

  private init() {
    this.buildingBlock.businesssEventCall$
      .pipe(
        takeUntil(this.disconnected$),
        tap((businessEvent) => {
          this.handleBusinessEvent(businessEvent);
        })
      )
      .subscribe();

    this.stateChangesObservable = this.buildingBlock.stateChange$.pipe(
      takeUntil(this.disconnected$),
      tap(() => {
        /*
          We need to get the latest state from `getState` method instead of using the emitted
          value from the observable. This is because the `stateChanged$` is a behavior subject
          with an empty initial value. This empty value will override the existing state.

          Because of this call, the `getState` method of building block will be invoked twice for every state
          change (once at abstract-building-block & once here)  and the solution given via
          https://github.developer.allianz.io/it-master-platform/itmp-frontend-workspace/pull/572)
          doesn't work well. TODO: We need to find a way that the `stateChanged$` behavior subject will
          emit the initial value from state if it is available instead of empty value every time.
        */
        this.storeBuildingBlockData(this.buildingBlock.getState());
      })
    );

    this.buildingBlock.navigateEvent$
      .pipe(
        takeUntil(this.disconnected$),
        tap((event) => {
          this.handleNavigation(event);
        })
      )
      .subscribe();

    if (this.buildingBlock.aclTag) {
      this.aclService
        .isHidden$(this.buildingBlock.aclTag.aclKey)
        .pipe(
          takeUntil(this.disconnected$),
          filter((value) => value === true),
          take(1),
          tap(() => this.storeBuildingBlockBackupData())
        )
        .subscribe();
    }

    this.buildingBlock.trackForm$.pipe(takeUntil(this.disconnected$)).subscribe((formGroup) => {
      if (formGroup && this.pfeTrackingService) {
        trackForm(
          formGroup,
          this.buildingBlock.id,
          this.disconnected$,
          this.pfeTrackingService.pushEvent.bind(this.pfeTrackingService)
        );
      }
    });
  }

  /**
   * @param {BusinessEvent} value
   */
  private handleBusinessEvent(businessEvent: BusinessEvent) {
    const buildingBlockBusinessEventConfiguration = this.buildingBlockMetaService.getBusinessEvents(
      this.buildingBlock.id
    );

    if (
      !buildingBlockBusinessEventConfiguration ||
      !(businessEvent.event in buildingBlockBusinessEventConfiguration)
    ) {
      console.warn(
        `Your given configuration for Building Block '${this.buildingBlock.id}' does not contain a business event called '${event}'. These are the configured business events for this Building Block:`,
        buildingBlockBusinessEventConfiguration
      );
      businessEvent.deferred.reject(
        new Error(
          `Business Event "${businessEvent.event}" does not exist for Building Block "${this.buildingBlock.id}".`
        )
      );
      return;
    }

    this.businessEventService.handleBusinessEvent(
      buildingBlockBusinessEventConfiguration[businessEvent.event],
      businessEvent.deferred
    );
  }

  private storeBuildingBlockData(data: unknown) {
    this._pfeBusinessService.storeValue(this.pfeStorageId, data);
  }

  private storeBuildingBlockBackupData() {
    // store current BB state in secret backup area
    this._pfeBusinessService.storeValueByExpression(
      this.pfeBackupStorageId,
      this.buildingBlock.getState()
    );
    // remove "public" BB state
    // (wrapped in a timeout to ensure that this is called *after* the
    // last call to `storeBuildingBlockData`)
    setTimeout(() => {
      this.storeBuildingBlockData(undefined);
    }, 0);
  }

  private getBuildingBlockBackupData() {
    const backupData = this._pfeBusinessService.getValueByExpression(this.pfeBackupStorageId);
    // remove backup after reading it
    this._pfeBusinessService.storeValueByExpression(this.pfeBackupStorageId, undefined);
    return backupData;
  }

  override connect() {
    console.log(`✅ PFE Facade connecting Building Block '${this.buildingBlock.id}'`);

    const form = this.buildingBlock.getForm();
    if (form && this.pfeTrackingService) {
      trackForm(
        form,
        this.buildingBlock.id,
        this.disconnected$,
        this.pfeTrackingService.pushEvent.bind(this.pfeTrackingService)
      );
    }

    let resourcesMap: BuildingBlockResourceMap | undefined;

    if (this.buildingBlockMetaService.hasResources(this.buildingBlock.id)) {
      resourcesMap = this.buildingBlockMetaService.getResources(
        this.buildingBlock.id
      ) as BuildingBlockResourceMap;
    }

    this.handleValidationConfiguration();
    // TODO: Connect can occur multiple times now
    // so initialize once and ensure that resources
    this.pfeResourcesService
      .handleResources(this.buildingBlock, resourcesMap)
      .pipe(
        // waiting for the `setResources` method to be invoked at least once before any other lifecycle
        // methods are invoked.
        tapOnce<StaticResourceList>(() => {
          this.handleState();
          super.connect();
        })
      )
      .subscribe();
  }

  /**
   * Implicit Page Navigation:
   * Check if we have an implicit navigation (relative or first page)
   * This will be a hard coded event in the Building Block and developers
   * can implement custom buttons that mimic the pfe actions buttons with those
   * types.
   *
   * Usage inside your Building Block:
   *  this.navigate(BUILDING_BLOCK_NAVIGATION_TYPE.Next);
   *  this.navigate(BUILDING_BLOCK_NAVIGATION_TYPE.Back);
   *  this.navigate(BUILDING_BLOCK_NAVIGATION_TYPE.Home);
   */
  private processImplicitNavigation(type: BUILDING_BLOCK_NAVIGATION_TYPE) {
    if (type === BUILDING_BLOCK_NAVIGATION_TYPE.Next) {
      this.frameNavigationService.navigateNext();
    }

    if (type === BUILDING_BLOCK_NAVIGATION_TYPE.Back) {
      this.frameNavigationService.navigateBack();
    }

    if (type === BUILDING_BLOCK_NAVIGATION_TYPE.Home) {
      this.frameNavigationService.navigateHome();
    }
  }

  /**
   * Explicit Page Navigation:
   * A page navigation event comes with a payload which contains a unique identifier
   * expressing the internal "keyword" the building blocks exposes in the navigation configuration.
   * The author of an application can map that internal value to an explicit value
   * we can forward to the PFE.
   *
   * INTERNAL PAGE ID -> EXTERNAL PAGE ID
   * e.g. contact -> page-b (where 'contact' is only known to the Building Block and page-b is an existing pfe id)
   *
   * The decoupling is technically required because the Building Block
   * is not allowed to encode fixed information. A page id "contact-page" is simply not existing
   * during the development of a Building Block. Someone needs to make sense of it later,
   * which is handled in the Building Block config.
   * ```
   * {
   *     "id": "buildingBlockInstanceA",
   *     "navigation": {
   *       "contact": "page-b",
   *       "my-other-codeword": "NAVIGATE_NEXT"
   *     }
   * }
   * ```
   */
  private processExplicitNavigation(type: BUILDING_BLOCK_NAVIGATION_TYPE, pageMappingId: string) {
    if (type === BUILDING_BLOCK_NAVIGATION_TYPE.Page) {
      const navigationData = this.buildingBlockMetaService.getNavigationData(this.buildingBlock.id);

      if (pageMappingId in navigationData) {
        const mappedPageIdValue = navigationData[pageMappingId];
        /**
         * If people chose to use one of the reserved keywords
         * for implicit navigation like NAVIGATE_NEXT or NAVIGATE_BACK
         * for the value given inside the pages.json (instead of an explicit page id)
         * then we want to handle it accordingly.
         * {
         *     "id": "buildingBlockInstanceA",
         *     "navigation": {
         *       "some-codeword": "NAVIGATE_NEXT",
         *       "something-else": "my-page-id",
         *     }
         * }
         */
        if (isImplicitNavigation(mappedPageIdValue)) {
          this.processImplicitNavigation(mappedPageIdValue);
        } else {
          this.frameNavigationService.gotoPage(mappedPageIdValue);
        }
      } else {
        console.warn(`PFE-Facade Navigation: no mapping found for id '${pageMappingId}'`);
      }
    }
  }

  private handleNavigation(event: BuildingBlockNavigationEvent) {
    if (event.type === BUILDING_BLOCK_NAVIGATION_TYPE.Page) {
      if (!event.payload) {
        throw new Error(
          `Building Block '${this.buildingBlock.id}' could not perform the navigation because a page was not specified.`
        );
      }

      this.processExplicitNavigation(event.type, event.payload);
    } else {
      this.processImplicitNavigation(event.type);
    }
  }

  private handleValidationConfiguration() {
    if (false === this.buildingBlockMetaService.hasData(this.buildingBlock.id, 'validators')) {
      return;
    }

    const validationRules = this.buildingBlockMetaService.getData(
      this.buildingBlock.id,
      'validators'
    ) as ValidationRule[];

    this.enrichAndSetValidationConfiguration(validationRules);
  }

  private enrichAndSetValidationConfiguration(validationRules: ValidationRule[]) {
    const pfeExpressionObservableQuery = pfeResources.pfeObservableExpressionBuilder(
      this._pfeBusinessService
    );

    const validationData = replacePFEQueryWithStream(validationRules, {
      pfeExpressionObservableQuery
    });
    this.buildingBlock.setValidationConfiguration(validationData);
  }

  private handleState() {
    const data = this._pfeBusinessService.getValue(this.pfeStorageId);
    const backupData = this.getBuildingBlockBackupData();
    if (backupData) {
      this.buildingBlock.state = {
        ...backupData,
        ...data
      };
    } else {
      this.buildingBlock.state = data;
    }
    this.stateChangesObservable.subscribe();
  }

  override disconnect() {
    console.log(`👣 PFE Facade disconnecting from Building Block '${this.buildingBlock.id}'`);
    super.disconnect();
  }
}

results matching ""

    No results matching ""