File

libs/core/src/lib/building-block/abstract-building-block.ts

Description

STATE: Highly fluctuating, expect many refactoring and breaking changes.

Our Building Blocks Foundation: The abstract Building Block definition. We need to prepare and implement everything to allow Building Block to deliver the following features:

  1. User State
  2. Resources (deliver required data like Dropdown lists, things to display only)
  3. Support Completion (Facade/PFE Integration)
  4. Support Various Lifecycle Methods: onPageConnection (via Facade)

Good things that might help us: A) Split different responsibilities into mixins B) Provide a Building Block Decorator, maybe that's easier than extending or beneficial in addition (to be checked) C) Keep Lifecycle out of the interface, it's internal knowledge in the end. D) Proper Code Inspection, extended use of Typescript to ensure: State is matching, Feature Flags are matching etc.

Implements

BuildingBlockInterface OnInit

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(injector: Injector)
Parameters :
Name Type Optional
injector Injector No

Inputs

id
Type : any
Default value : dasherize(this.constructor.name)

can be changed during runtime and given as a default by the implemented Building Block

resources
Type : BuildingBlockResources

The method will be called by the facade and is marked as an @Input. Derived Building Blocks need to list the input in the @Component decorator under inputs:[] to make it available to the template (View Engine limitation).

state
Type : NoArray<BuildingBlockState>

The method will be called by the facade and is marked as an @Input. Derived Building Blocks need to list the input in the @Component decorator under inputs:[] to make it available to the template (View Engine limitation).

Outputs

completed
Type : EventEmitter

Mark this Building Block as complete. That way a workflow engine can determine the overall completion state of any given pages. This method can be actively triggered by the user (say a button is clicked) or indirectly by a form being changed.

The author of a Building Block needs to make sure that this function is called during the lifetime of a Building Block. If the Building Block is purely presentational this method can be called during initialization.

Methods

callBusinessEvent
callBusinessEvent(event: string)

This method creates a Deferred object for the business event given. This will have a newly created promise and their executor parameters resolve & reject. This deferred object is then emitted along with the business event name

The Store Integration(Facade) can then decide when to resolve or reject the underlying promise based on the service activator call attached to the business event.

call's status.

Parameters :
Name Type Optional Description
event string No

name of the business event

Returns : Promise<>

the promise from the deferred. The building block can then be notified on the respective service activator call's status.

commitCompletion
commitCompletion()
Returns : void
getForm
getForm()

This is supposed to return the building block's form. Leave this untouched if your building block implementation does not have a form.

getState
getState()

⚠️ If this function shows an error that contains Type '<some-type>[]' is not assignable to type 'never'., it means that you are trying to use an Array as your BB's State. This is not allowed. Please use an object instead.

Retrieve the current state of this Building Block. It's an empty object by default. The state should only include data that is useful to the outside world. Don't expose internal states like scroll position, button press states etc. Think of it as data that is useful in an overall business process and that you would also recognize in written insurance contracts for example.

The author of a Building Block needs to implement this function in order to deliver a proper snapshot of the current state of all components and/or forms.

navigate
navigate(type: BUILDING_BLOCK_NAVIGATION_TYPE, payload?: string)
Parameters :
Name Type Optional
type BUILDING_BLOCK_NAVIGATION_TYPE No
payload string Yes
Returns : void
onPageConnection
onPageConnection()
Returns : void
onPageDisconnected
onPageDisconnected()
Returns : void
revertCompletion
revertCompletion()
Returns : void
setResources
setResources(data: BuildingBlockResources | undefined)
Parameters :
Name Type Optional
data BuildingBlockResources | undefined No
Returns : void
setState
setState(state: NoArray<BuildingBlockState>)
Parameters :
Name Type Optional
state NoArray<BuildingBlockState> No
Returns : void
setValidationConfiguration
setValidationConfiguration(data: ValidationConfigItem[])
Parameters :
Name Type Optional
data ValidationConfigItem[] No
Returns : void
stateChanged
stateChanged()
Returns : void
transformResources
transformResources(data: BuildingBlockResources)

Override if you need to pre process the incoming resource data and if you want to establish some default values. The return value will arrive in setResources and stored in the 'resources' variable. Needs to match the Resource Generic of the given Building Block

Parameters :
Name Type Optional
data BuildingBlockResources No
transformState
transformState(data: NoArray<BuildingBlockState>)

Override if you need to pre process the incoming state data and if you want to establish some default values. The return value will arrive in setState and stored in the 'state' variable. Needs to match the State Generic of the given Building Block

Parameters :
Name Type Optional
data NoArray<BuildingBlockState> No

Properties

_aclService
Type : AclService
aclTag
Type : AclTag | null
businesssEventCall$
Default value : new EventEmitter<BusinessEvent>()
Readonly channel
Type : CHANNEL
completion$
Default value : new BehaviorSubject(false)
connected$
Default value : new Subject<void>()

Lifecycle Event called once the Building Block is connected to the page. This can only happen when the Building Block is embed in a Building Block Page This is not intended to be used standalone.

At this point you can be sure that:

  1. The Building Block is ready (AfterViewInit)
  2. The initial state and resources has been set
disconnected$
Default value : new Subject<void>()
Readonly isExpertChannel
Type : boolean
Readonly isRetailChannel
Type : boolean
loadingStatus$
Default value : this._loadingStatus$ .asObservable() .pipe( scan(accumulateBusinessEvents, []), map(groupEventStatusesByName), distinctUntilChanged(isEqual) )
navigateEvent$
Default value : new EventEmitter<BuildingBlockNavigationEvent>()
stateChange$
Default value : new BehaviorSubject({} as BuildingBlockState)

Call this function to grab the current state and forward it any time your internal state is ready to be forwarded. It's a BehaviorSubject to always deliver the latest state on subscription. TODO: Evaluate if a multicast could help maintaining an more coordiated data stream

trackForm$
Default value : new EventEmitter<UntypedFormGroup>()

Can be used to add automatic tracking to a form. Keep in mind that the form that is returned by getForm() is tracked automatically during the initialization.

This only has to be called if there are other forms that should be tracked or if the form is created after the initialization.

Accessors

resources
getresources()
setresources(data: BuildingBlockResources)

The method will be called by the facade and is marked as an @Input. Derived Building Blocks need to list the input in the @Component decorator under inputs:[] to make it available to the template (View Engine limitation).

Parameters :
Name Type Optional
data BuildingBlockResources No
Returns : void
state
getstate()
setstate(data: NoArray<BuildingBlockState>)

The method will be called by the facade and is marked as an @Input. Derived Building Blocks need to list the input in the @Component decorator under inputs:[] to make it available to the template (View Engine limitation).

Parameters :
Name Type Optional
data NoArray<BuildingBlockState> No
Returns : void
acl
getacl()
import { AclEvaluationInterface, AclTag } from '@allianz/taly-acl';
import { ACL_TAG_TOKEN, AclService, wrapAclEvaluationWithTag } from '@allianz/taly-acl/angular';
import { Directive, EventEmitter, Inject, Injector, Input, OnInit, Output } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import isEqual from 'lodash/isEqual';
import { BehaviorSubject, Subject } from 'rxjs';
import { distinctUntilChanged, map, scan, take } from 'rxjs/operators';
import { ValidationConfigItem } from '../form-support/validation.model';
import { CHANNEL } from '../models/model';
import { CHANNEL_TOKEN } from '../tokens';
import { dasherize } from '../utils';
import { BuildingBlockInterface, BusinessEvent, Deferred } from './building-block-interface';
import { BUILDING_BLOCK_NAVIGATION_TYPE, BuildingBlockNavigationEvent } from './navigation';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type NoArray<T> = T extends any[] ? never : T;

/**
 * Why don't we use Record<string, any> here?
 * Unfortunately, this is not fully supported by the json schema generator.
 * With the usage of Record, the type safety there is lost.
 *
 * See also: https://github.com/YousefED/typescript-json-schema/issues/547
 *
 * (In reality, this does not make a real difference in the AbstractBuildingBlock,
 * but it is what Building Blocks should also do)
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type RecordObjectLike = Record<string, any>;

/**
 * STATE: Highly fluctuating, expect many refactoring and breaking changes.
 *
 * Our Building Blocks Foundation: The abstract Building Block definition.
 * We need to prepare and implement everything to allow Building Block to deliver the following features:
 *
 * 1. User State
 * 2. Resources (deliver required data like Dropdown lists, things to display only)
 * 3. Support Completion (Facade/PFE Integration)
 * 4. Support Various Lifecycle Methods: onPageConnection (via Facade)
 *
 * Good things that might help us:
 * A) Split different responsibilities into mixins
 * B) Provide a Building Block Decorator, maybe that's easier than extending or beneficial in addition (to be checked)
 * C) Keep Lifecycle out of the interface, it's internal knowledge in the end.
 * D) Proper Code Inspection, extended use of Typescript to ensure:
 *     State is matching, Feature Flags are matching etc.
 */
@Directive()
export class AbstractBuildingBlock<
  BuildingBlockState extends RecordObjectLike | undefined = Record<string, unknown>,
  BuildingBlockResources = Record<string, unknown> | undefined
> implements BuildingBlockInterface<BuildingBlockState, BuildingBlockResources>, OnInit
{
  _aclService: AclService;
  aclTag: AclTag | null;

  readonly channel: CHANNEL;
  readonly isRetailChannel: boolean;
  readonly isExpertChannel: boolean;

  businesssEventCall$ = new EventEmitter<BusinessEvent>();
  navigateEvent$ = new EventEmitter<BuildingBlockNavigationEvent>();

  /**
   * Can be used to add automatic tracking to a form. Keep in mind that the form that is returned by getForm()
   * is tracked automatically during the initialization.
   *
   * This only has to be called if there are other forms that should be tracked or if the form
   * is created after the initialization.
   */
  trackForm$ = new EventEmitter<UntypedFormGroup>();

  private isConnected = false;

  constructor(@Inject(Injector) injector: Injector) {
    this._aclService = injector.get<AclService>(AclService);
    this.aclTag = injector.get<AclTag | null>(ACL_TAG_TOKEN, null);

    this.channel = injector.get<CHANNEL>(CHANNEL_TOKEN);
    this.isRetailChannel = this.channel === CHANNEL.RETAIL;
    this.isExpertChannel = this.channel === CHANNEL.EXPERT;

    this.connected$.pipe(take(1)).subscribe(() => (this.isConnected = true));
    this.disconnected$.pipe(take(1)).subscribe(() => (this.isConnected = false));
  }

  /**
   * This method creates a Deferred object for the business event given. This will have a newly created promise and their
   * executor parameters resolve & reject. This deferred object is then emitted along with the business event name
   *
   * The Store Integration(Facade) can then decide when to resolve or reject the underlying promise based on the service
   * activator call attached to the business event.
   *
   * @param event name of the business event
   * @returns the promise from the deferred. The building block can then be notified on the respective service activator
   * call's status.
   */
  callBusinessEvent(event: string): Promise<unknown> {
    const isJestEnvironment = 'test' in globalThis && 'describe' in globalThis;
    if (!this.isConnected && !isJestEnvironment) {
      throw new Error(
        `
Can't call business event "${event}" for Building Block "${this.id}" because it's not yet connected to the page.
A Building Block must be connected to a page for it to be able to call 'this.callBusinessEvent()'.
You're likely calling 'this.callBusinessEvent("${event}")' from inside the Building Block's constructor or 'ngOnInit()' method.
Please move the call to 'this.callBusinessEvent()' into the Building Block's 'onPageConnection()' method.
`
      );
    }
    const deferred = createDeferred();
    this._loadingStatus$.next({ event, loading: true });
    this.businesssEventCall$.emit({ event, deferred });
    return deferred.promise.finally(() => this._loadingStatus$.next({ event, loading: false }));
  }

  navigate(type: BUILDING_BLOCK_NAVIGATION_TYPE, payload?: string) {
    const event: BuildingBlockNavigationEvent = {
      type
    };

    if (payload) {
      event.payload = payload;
    }

    this.navigateEvent$.emit(event);
  }

  /**
   * can be changed during runtime and given as a default
   * by the implemented Building Block
   */
  @Input() id = dasherize(this.constructor.name);

  // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method
  ngOnInit(): void {}

  /**
   * Mark this Building Block as complete.
   * That way a workflow engine can determine the overall completion state of any given pages.
   * This method can be actively triggered by the user (say a button is clicked) or indirectly by
   * a form being changed.
   *
   * The author of a Building Block needs to make sure that this function is called during the lifetime of a Building Block.
   * If the Building Block is purely presentational this method can be called during initialization.
   *
   */

  // That's useful to hook into the state while a Building Block is mounted in a template.
  @Output() completed = new EventEmitter();
  // have a separate Behaviour Subject to always reflect the current state of completion.
  completion$ = new BehaviorSubject(false);

  commitCompletion() {
    this.completion$.next(true);
    this.completed.emit(true);
  }

  revertCompletion() {
    this.completion$.next(false);
    this.completed.emit(false);
  }

  /**
   * Lifecycle Event called once the Building Block is connected to the page.
   * This can only happen when the Building Block is embed in a Building Block Page
   * This is not intended to be used standalone.
   *
   * At this point you can be sure that:
   * 1. The Building Block is ready (AfterViewInit)
   * 2. The initial state and resources has been set
   */
  connected$ = new Subject<void>();
  onPageConnection() {}

  disconnected$ = new Subject<void>();
  onPageDisconnected() {}

  setValidationConfiguration(data: ValidationConfigItem[]) {}
  /**
   * RESOURCE HANDLING
   */

  /**
   * Provide data that is necessary for the Building Blocks to work.
   * This can be lists of choices (for dropdowns) but also image urls, and other data
   * that is not changeable by the user. It's pretty static although it could change depending
   * of a user's choice (like a new set of dropdown values). This means there is a chance that the data will change during runtime
   *
   * This is very different from the Building Block state which can be changed by the user
   * and can highly fluctuate between users.
   *
   * setResources is the internal hook that can be overwritten by a specific implementation.
   * The getter & setter are meant as fixed implementations ensuring a proper order of invocations & storage of data
   */
  private _resources!: BuildingBlockResources;

  /**
   * Override if you need to pre process the incoming resource data
   * and if you want to establish some default values. The return value will arrive in setResources
   * and stored in the 'resources' variable. Needs to match the Resource Generic of the given Building Block
   */
  transformResources(data: BuildingBlockResources): BuildingBlockResources {
    return data;
  }

  /**
   * The method will be called by the facade
   * and is marked as an @Input. Derived Building Blocks need to list
   * the input in the @Component decorator under `inputs:[]` to make it available to the template (View Engine limitation).
   */
  @Input()
  set resources(data: BuildingBlockResources) {
    this._resources = this.transformResources(data);
    this.setResources(this._resources);
  }
  get resources(): BuildingBlockResources {
    return this._resources;
  }

  setResources(data: BuildingBlockResources | undefined) {}

  /**
   * STATE HANDLING
   */

  /**
   * Call this function to grab the current state and forward it
   * any time your internal state is ready to be forwarded.
   * It's a BehaviorSubject to always deliver the latest state on subscription.
   * TODO: Evaluate if a multicast could help maintaining an more coordiated data stream
   */
  stateChange$ = new BehaviorSubject({} as BuildingBlockState);
  stateChanged() {
    this.stateChange$.next(this.getState());
  }

  /**
   * Restore a previous state of this Building Block by
   * passing in a state object the Building Block can use to
   * hydrate the according components & forms.
   *
   * The author of a Building Block needs to override this function
   * in order to process the given state properly.
   */
  private _state!: NoArray<BuildingBlockState>;

  /**
   * Override if you need to pre process the incoming state data
   * and if you want to establish some default values. The return value will arrive in setState
   * and stored in the 'state' variable. Needs to match the State Generic of the given Building Block
   */
  transformState(data: NoArray<BuildingBlockState>): NoArray<BuildingBlockState> {
    return data;
  }

  /**
   * The method will be called by the facade
   * and is marked as an @Input. Derived Building Blocks need to list
   * the input in the @Component decorator under `inputs:[]` to make it available to the template (View Engine limitation).
   */
  @Input()
  set state(data: NoArray<BuildingBlockState>) {
    this._state = this.transformState(data);
    this.setState(this._state);
  }

  get state(): NoArray<BuildingBlockState> {
    return this._state;
  }

  setState(state: NoArray<BuildingBlockState>) {}
  /**
   * ⚠️ If this function shows an error that contains `Type '<some-type>[]' is not assignable to type 'never'.`,
   * it means that you are trying to use an Array as your BB's State. This is not allowed. Please use an object instead.
   *
   * Retrieve the current state of this Building Block. It's an empty object by default.
   * The state should only include data that is useful to the outside world. Don't expose internal states
   * like scroll position, button press states etc. Think of it as data that is useful in an overall business
   * process and that you would also recognize in written insurance contracts for example.
   *
   * The author of a Building Block needs to implement this function
   * in order to deliver a proper snapshot of the current state of all components and/or forms.
   */
  getState(): NoArray<BuildingBlockState> {
    // TODO: That needs to throw an error if T is defined but getState is not overwritten.
    return {} as NoArray<BuildingBlockState>;
  }

  get acl(): AclEvaluationInterface {
    /**
     * we need to expose our AclService bound to the given aclTag
     * of this instance if any. This ensures that calls to
     * this.acl.canShow will incorporate the given acl tag hierarchy.
     * This is important for anything you do manually in the template
     * or for supporting scripts like `autoFormBindingFactory`
     */
    if (this.aclTag) {
      return wrapAclEvaluationWithTag(this._aclService, this.aclTag);
    }

    return this._aclService;
  }

  /**
   * This is supposed to return the building block's form.
   * Leave this untouched if your building block implementation does not have a form.
   */
  getForm(): UntypedFormGroup | undefined {
    return undefined;
  }

  private _loadingStatus$ = new Subject<BusinessEventStatus>();

  loadingStatus$ = this._loadingStatus$
    .asObservable()
    .pipe(
      scan(accumulateBusinessEvents, []),
      map(groupEventStatusesByName),
      distinctUntilChanged(isEqual)
    );
}

interface BusinessEventStatus {
  event: string;
  loading: boolean;
}

function accumulateBusinessEvents(arr = [] as BusinessEventStatus[], event: BusinessEventStatus) {
  return [...arr, event];
}

function groupEventStatusesByName(arr: BusinessEventStatus[] = []) {
  const grouped = arr.reduce((status, { event, loading }) => {
    status[event] ??= 0;
    status[event] += loading ? 1 : -1;
    return status;
  }, {} as Record<string, number>);

  return Object.entries(grouped).reduce((status: Record<string, boolean>, [event, count]) => {
    status[event] = count > 0;
    return status;
  }, {});
}

function createDeferred(): Deferred {
  const deferred = {} as Deferred;
  deferred.promise = new Promise((resolve, reject) => {
    deferred.resolve = resolve;
    deferred.reject = reject;
  });
  return deferred;
}

results matching ""

    No results matching ""