File

libs/core/dynamic-form/src/base/base.component.ts

Description

Base component class for formfield components.

A formfield is a labeled input of some kind that lets the user input or select a single value. Components deriving from this base class work exclusively with reactive forms, so a formfield's model is therefore always an AbstractControl, which might bei either a concrete FormControl or a more complex subclass like a FormGroup or FormArray.

Examples are labelled text inputs, dropdowns, checkboxes, groups of radio buttons and so on.

Formfield-like components should extend from this class wherever possible. This ensures a more consistent API for inputs & outputs. Furthermore, it makes them compatible with the dynamic formfield component.

This base class takes care of processing the validator configurations and adding the necessary validators to the underlying AbstractControl. Derived formfield components should use the errorMessages variable to retrieve the error text of whichever validator is failing and display it to the user.

Implements

OnInit

Index

Properties
Inputs
Outputs
Accessors

Inputs

config
Type : C

The configuration object for this formfield.

Note that derived formfield components should extend the DfBaseConfig config interface as needed and expose that their own config interface.

control
Type : AbstractControl
Default value : new UntypedFormControl()

The AbstractControl (to be) associated with this formfield.

If an existing AbstractControl is supplied, this component will associate itself with it, set its initial value (if given in the config), and add any validator functions specified in the config.

If no AbstractControl instance is supplied, this component will create a FormControl itself.

If a form component doesn't use the AbstractControl, it should be set to undefined in the extending class.

validationConfigs
Type : ValidationConfig[]

Outputs

controlChanged
Type : EventEmitter

Emits the AbstractControl associated with this formfield, once it has been fully configured (i.e. its initial value and validators have been added as per the config).

Applications that need to further process this AbstractControl (e.g. to add more validators) should therefore wait for this event to be emitted.

controlInitFinished
Type : ReplaySubject

Additionally to the controlChanged EventEmitter a ReplaySubject is provided. In difference to the EventEmitter, this one always contains the last emitted value, which allows to check if a component was initialized after the initialization already happened. (An event will be gone at that point in time.)

formEvent
Type : EventEmitter

Emits when events associated to the form control happen.

The emitted object contains the data necessary to uniquely identify the event (field id and event type). It also contains the event data.

Properties

aclResource
Type : string
Optional isRetailChannel
Type : boolean

Accessors

config
getconfig()
setconfig(value: C)

The configuration object for this formfield.

Note that derived formfield components should extend the DfBaseConfig config interface as needed and expose that their own config interface.

Parameters :
Name Type Optional
value C No
Returns : void
import { ValidationConfig } from '@allianz/taly-core';
import { dasherize } from '@angular-devkit/core/src/utils/strings';
import { Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
  AbstractControl,
  UntypedFormArray,
  UntypedFormControl,
  UntypedFormGroup
} from '@angular/forms';
import { debounceTime, ReplaySubject, skip } from 'rxjs';
import { DfValueProviderService } from '../services/value-provider/value-provider.service';
import { DfBaseConfig, type DfEventPayload } from './base.model';

let lineBreakCounter = 0;

/**
 * Base component class for formfield components.
 *
 * A formfield is a labeled input of some kind that lets the user input or select a single
 * value. Components deriving from this base class work exclusively with reactive forms, so
 * a formfield's model is therefore always an `AbstractControl`, which might bei either a concrete
 * `FormControl` or a more complex subclass like a `FormGroup` or `FormArray`.
 *
 * Examples are labelled text inputs, dropdowns, checkboxes, groups of radio buttons and so on.
 *
 * Formfield-like components should extend from this class wherever possible. This ensures a
 * more consistent API for inputs & outputs. Furthermore, it makes them compatible with the
 * dynamic formfield component.
 *
 * This base class takes care of processing the validator configurations and adding the necessary
 * validators to the underlying `AbstractControl`. Derived formfield components should use the
 * `errorMessages` variable to retrieve the error text of whichever validator is failing and
 * display it to the user.
 */
@Component({
  template: ''
})
export class DfBaseComponent<C extends DfBaseConfig = DfBaseConfig> implements OnInit {
  private valueProviderService: DfValueProviderService | null = inject(DfValueProviderService, {
    optional: true
  });
  private _config!: C;
  private destroyRef = inject(DestroyRef);

  aclResource!: string;

  /**
   * The configuration object for this formfield.
   *
   * Note that derived formfield components should extend the `DfBaseConfig` config interface
   * as needed and expose that their own config interface.
   */
  @Input() set config(value: C) {
    this._config = value;
    if (value.type !== 'LINE_BREAK') {
      this.aclResource = dasherize(this._config.id);
    } else {
      this.aclResource = `line-break-${lineBreakCounter++}`;
    }
  }

  // TODO: Replace this getter/setter approach here with something else as it is called on every change detection cycle
  get config(): C {
    return this._config;
  }

  /**
   * The `AbstractControl` (to be) associated with this formfield.
   *
   * If an existing `AbstractControl` is supplied, this component will associate itself
   * with it, set its initial value (if given in the config), and add
   * any validator functions specified in the config.
   *
   * If no `AbstractControl` instance is supplied, this component will create a `FormControl` itself.
   *
   * If a form component doesn't use the `AbstractControl`, it should be set to undefined
   * in the extending class.
   */
  @Input() control?: AbstractControl = new UntypedFormControl();

  @Input() validationConfigs?: ValidationConfig[];

  /**
   * Emits the `AbstractControl` associated with this formfield, once it has been fully
   * configured (i.e. its initial value and validators have been
   * added as per the config).
   *
   * Applications that need to further process this `AbstractControl` (e.g. to add more
   * validators) should therefore wait for this event to be emitted.
   */
  // Using async EventEmitter here to avoid ExpressionChangedAfterItHasBeenCheckedError
  // errors are thrown by parent components, when ngOnInit() emits this event.
  // (See: https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4)
  @Output() controlChanged = new EventEmitter<AbstractControl>(true);

  /**
   * Emits when events associated to the form control happen.
   *
   * The emitted object contains the data necessary to uniquely identify the event (field id and event type). It also contains the event data.
   */
  @Output() formEvent = new EventEmitter<DfEventPayload>();

  /**
   * Additionally to the controlChanged EventEmitter a ReplaySubject is provided.
   * In difference to the EventEmitter, this one always contains the last emitted value,
   * which allows to check if a component was initialized after the initialization already happened.
   * (An event will be gone at that point in time.)
   */
  @Output() controlInitFinished = new ReplaySubject<AbstractControl>(1);

  isRetailChannel?: boolean;

  /**
   * By default, the `AbstractControl` is configured in ngOnInit(), however
   * in some cases this causes an `ExpressionChangedAfterItHasBeenCheckedError`.
   *
   * If this is set to true, the setup will **not** be run automatically. It is then
   * the derived component's responsibility to run call `setupFormControl()` at the
   * appropriate time.
   */
  protected deferSetupControl = false;

  /**
   * Sets up the `AbstractControl` using the config.
   *
   * Unless `deferSetupControl` was set to `true`, in which
   * the the only action that is taken is to create a new concrete
   * `AbstractControl` instance if none was provided as an input.
   *
   * This is needed as the template cannot render unless
   * `control` is defined.
   */
  ngOnInit() {
    this.controlChanged.subscribe(this.controlInitFinished);
    if (!this.deferSetupControl) {
      this.setupFormControl();
    }
  }

  /**
   * Sets this field's form control value to the initial value from the config.
   *
   * If the group does not already contain a form control for this field, a
   * new one is created and added.
   */
  protected setupFormControl = () => {
    // Use patchValue instead of setValue in order to support incomplete
    // values supplied via config for complex form groups.

    // Perform any additional setup that may be required
    this.doAdditionalSetup();

    // If there is no configured value, we use a default value depending on
    // the concrete type of the AbstractControl.
    // Otherwise the form control won't register any kind of update to the view.
    if (!('value' in this.config)) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      let defaultValue: any;
      if (this.control instanceof UntypedFormGroup) {
        defaultValue = {};
      } else if (this.control instanceof UntypedFormArray) {
        defaultValue = [];
      } else {
        defaultValue = null;
      }
      this.control?.patchValue(defaultValue);
      this.controlChanged.emit(this.control);
      return;
    }

    if (this.valueProviderService) {
      this.valueProviderService
        .getValue(this.config.value)
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe((value) => {
          this.control?.patchValue(value);
          this.controlChanged.emit(this.control);
        });
    } else {
      this.control?.patchValue(this.config.value);
      this.controlChanged.emit(this.control);
    }
  };

  /**
   * Is called when this field's form control is set up.
   *
   * The default implementation is a no-op. Sub-classes can override
   * it with any extra setup steps they need to perform.
   */
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  protected doAdditionalSetup() {}

  /**
   * Helper function to emit an event from the form field.
   * Mostly meant for Dynamic Form custom components to emit their custom events.
   *
   * @param {string} type The type of the event, as specified in the configuration of the dynamic form field
   * @param {unknown} [data]    The event data
   */
  protected emitFormEvent(type: string, data?: unknown) {
    this.formEvent.emit({
      type,
      fieldIdConfig: this.config.id,
      data
    });
  }

  /**
   * Helper function to emit an event when the value of the form control associated to the form field changes.
   */
  protected emitFormControlEventOnValueChanges() {
    if (!this.control) return;

    // Skip initialisation event
    this.control.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef), skip(1))
      .subscribe((newValue) => this.emitFormEvent('onValueChangesEvent', newValue));
  }

  /**
   * Helper function to emit an event when the value of the form control associated to the form field changes.
   *
   * @param {number} debounceTimeInMs The debouncing time applied to prevent too many emissions. By default it is 300ms.
   */
  protected emitFormControlEventOnValueChangesWithDebounce(debounceTimeInMs = 300) {
    if (!this.control) return;

    // Skip initialisation event
    this.control.valueChanges
      .pipe(takeUntilDestroyed(this.destroyRef), skip(1), debounceTime(debounceTimeInMs))
      .subscribe((newValue) => this.emitFormEvent('onValueChangesEvent', newValue));
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""