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

Constructor

constructor()

Properties

aclResource
Type : string
componentOrControlInitFinished
Default value : new ReplaySubject<AbstractControl | undefined>(1)

This ReplaySubject is provided to emit the control once it is initialized.

config
Type : InputSignal<C>
Default value : input.required<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 : InputSignal<AbstractControl> | InputSignal<UntypedFormControl> | InputSignal<UntypedFormGroup> | InputSignal<undefined>
Default value : input<AbstractControl>(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.

Note that if undefined is explicitly provided e.g. in the DfHeadlineComponent or DfParagraphComponent, the component will NOT create a FormControl itself.

formAclPath
Default value : input<string>()
Readonly formEvent
Default value : output<DfEventPayload>()

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.

isRetailChannel
Default value : input<boolean>()
validationConfigs
Default value : input<ValidationConfig[] | undefined>()
import { ValidationConfig } from '@allianz/taly-core';
import { dasherize } from '@angular-devkit/core/src/utils/strings';
import {
  Component,
  DestroyRef,
  effect,
  inject,
  input,
  InputSignal,
  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: '',
  standalone: false
})
export class DfBaseComponent<C extends DfBaseConfig = DfBaseConfig> implements OnInit {
  private valueProviderService: DfValueProviderService | null = inject(DfValueProviderService, {
    optional: true
  });

  protected 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.
   */
  config: InputSignal<C> = input.required<C>();

  /**
   * 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.
   *
   * Note that if `undefined` is explicitly provided e.g. in the `DfHeadlineComponent` or `DfParagraphComponent`, the component will NOT create a `FormControl` itself.
   */

  control:
    | InputSignal<AbstractControl>
    | InputSignal<UntypedFormControl>
    | InputSignal<UntypedFormGroup>
    | InputSignal<undefined> = input<AbstractControl>(new UntypedFormControl());

  validationConfigs = input<ValidationConfig[] | undefined>();

  formAclPath = input<string>();

  isRetailChannel = input<boolean>();

  /**
   * 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.
   */
  readonly formEvent = output<DfEventPayload>();

  /**
   * This ReplaySubject is provided to emit the control once it is initialized.
   */
  componentOrControlInitFinished = new ReplaySubject<AbstractControl | undefined>(1);

  constructor() {
    effect(() => {
      const configValue = this.config();

      if (configValue?.type !== 'LINE_BREAK') {
        this.aclResource = dasherize(configValue.id);
      } else {
        this.aclResource = `line-break-${lineBreakCounter++}`;
      }
    });
  }

  /**
   * Sets up the `AbstractControl` using the config.
   * This is needed as the template cannot render unless `control` is defined.
   */
  ngOnInit() {
    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();

    const configValue = this.config();

    // 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 configValue)) {
      // 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);
    } else if (this.valueProviderService) {
      this.valueProviderService
        .getValue(configValue.value)
        .pipe(takeUntilDestroyed(this.destroyRef))
        .subscribe((value) => {
          this.control()?.patchValue(value);
        });
    } else {
      this.control()?.patchValue(configValue.value);
    }

    // The controlChanged output was an asynchronous EventEmitter in the past. With the migration
    // to an output function, this changed to be synchronous. Then it was consolidated into componentOrControlInitFinished
    // Using a setTimeout here ensures that the behavior is the same as with the previous asynchronous EventEmitter.
    // Unlike the subscribe in the formfield component, this one did not cause an ExpressionChangedAfterItHasBeenCheckedError,
    // but this still ensures that the behavior is the same as before.
    setTimeout(() => {
      this.componentOrControlInitFinished.next(this.control());
    }, 0);
  };

  /**
   * 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() {
    const controlValue = this.control();
    if (!controlValue) return;

    // Skip initialization event
    controlValue.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) {
    const controlValue = this.control();
    if (!controlValue) return;

    // Skip initialisation event
    controlValue.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 ""