libs/core/dynamic-form/src/base/base.component.ts
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.
Properties |
|
Inputs |
Outputs |
Accessors |
control | |
Type : AbstractControl
|
|
Default value : new UntypedFormControl()
|
|
The If an existing If no If a form component doesn't use the |
validationConfigs | |
Type : ValidationConfig[]
|
|
controlChanged | |
Type : EventEmitter
|
|
Emits the Applications that need to further process this |
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. |
aclResource |
Type : string
|
Optional isRetailChannel |
Type : boolean
|
config | ||||||
getconfig()
|
||||||
setconfig(value: C)
|
||||||
The configuration object for this formfield. Note that derived formfield components should extend the
Parameters :
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));
}
}