File
Implements
Index
Properties
|
|
HostBindings
|
|
Accessors
|
|
HostBindings
class.df-spacing-none
|
Type : boolean
|
|
class.df-spacing-xs
|
Type : boolean
|
|
Defines the spacing of the formfield.
|
class.df-spacing-xxl
|
Type : boolean
|
|
componentOrControlInitFinished
|
Default value : new ReplaySubject<void>(1)
|
|
Additionally to the groupChanged 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.)
This subject will fire with an undefined value, after the configuration was passed to the component,
if the component doesn't have a form control.
|
config
|
Default value : input<DfBaseConfig>()
|
|
Optional
containerRef
|
Type : ViewContainerRef
|
Decorators :
@ViewChild('container', {read: ViewContainerRef, static: true})
|
|
defaultSpacing
|
Default value : input<DfFormfieldSpacing>()
|
|
formAclPath
|
Default value : input<string>()
|
|
Readonly
formEvent
|
Default value : output<DfEventPayload>()
|
|
group
|
Type : UntypedFormGroup
|
Default value : new UntypedFormGroup({})
|
|
isRetailChannel
|
Default value : input.required<boolean>()
|
|
validationConfigs
|
Default value : input<ValidationConfig[]>()
|
|
Accessors
isSpacingXS
|
getisSpacingXS()
|
|
Defines the spacing of the formfield.
|
isSpacingS
|
getisSpacingS()
|
|
isSpacingM
|
getisSpacingM()
|
|
isSpacingL
|
getisSpacingL()
|
|
isSpacingXL
|
getisSpacingXL()
|
|
isSpacingXXL
|
getisSpacingXXL()
|
|
isSpacingNone
|
getisSpacingNone()
|
|
import { ValidationConfig } from '@allianz/taly-core';
import {
Component,
ComponentRef,
effect,
HostBinding,
Injector,
input,
OnDestroy,
OnInit,
output,
signal,
ViewChild,
ViewContainerRef,
WritableSignal
} from '@angular/core';
import { outputToObservable } from '@angular/core/rxjs-interop';
import { AbstractControl, FormGroupDirective, UntypedFormGroup } from '@angular/forms';
import { ReplaySubject, Subject, takeUntil } from 'rxjs';
import { DfBaseComponent } from '../base/base.component';
import { DfBaseConfig, type DfEventPayload, DfFormfieldSpacing } from '../base/base.model';
import { DfComponentLoaderService } from './../services/component-loader/component-loader.service';
@Component({
selector: 'df-formfield',
templateUrl: './formfield.component.html',
styleUrls: ['./formfield.component.scss'],
standalone: false
})
export class DfFormfieldComponent implements OnInit, OnDestroy {
config = input<DfBaseConfig>();
private componentInstance: WritableSignal<DfBaseComponent<DfBaseConfig> | undefined> =
signal(undefined);
validationConfigs = input<ValidationConfig[]>();
formAclPath = input<string>();
defaultSpacing = input<DfFormfieldSpacing>();
isRetailChannel = input.required<boolean>();
@ViewChild('container', { read: ViewContainerRef, static: true }) containerRef?: ViewContainerRef;
readonly formEvent = output<DfEventPayload>();
/**
* Additionally to the groupChanged 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.)
*
* This subject will fire with an undefined value, after the configuration was passed to the component,
* if the component doesn't have a form control.
*/
componentOrControlInitFinished = new ReplaySubject<void>(1);
/**
* Defines the spacing of the formfield.
*/
@HostBinding('class.df-spacing-xs') get isSpacingXS() {
return this.checkFormfieldSpacing(DfFormfieldSpacing.xs);
}
@HostBinding('class.df-spacing-s') get isSpacingS() {
return this.checkFormfieldSpacing(DfFormfieldSpacing.s);
}
@HostBinding('class.df-spacing-m') get isSpacingM() {
return this.checkFormfieldSpacing(DfFormfieldSpacing.m);
}
@HostBinding('class.df-spacing-l') get isSpacingL() {
return this.checkFormfieldSpacing(DfFormfieldSpacing.l);
}
@HostBinding('class.df-spacing-xl') get isSpacingXL() {
return this.checkFormfieldSpacing(DfFormfieldSpacing.xl);
}
@HostBinding('class.df-spacing-xxl') get isSpacingXXL() {
return this.checkFormfieldSpacing(DfFormfieldSpacing.xxl);
}
@HostBinding('class.df-spacing-none') get isSpacingNone() {
return this.checkFormfieldSpacing(DfFormfieldSpacing.none);
}
private tearDownComponentSubscriptions$ = new Subject<void>();
group: UntypedFormGroup = new UntypedFormGroup({});
private formControl?: AbstractControl;
component?: ComponentRef<DfBaseComponent>;
constructor(
private injector: Injector,
private componentService: DfComponentLoaderService,
private parentFormGroup: FormGroupDirective
) {
effect(() => {
const componentInstance = this.componentInstance();
if (componentInstance && this.validationConfigs()) {
this.component?.setInput('validationConfigs', this.validationConfigs());
}
});
}
ngOnInit() {
this.group = this.parentFormGroup.form;
// The ngOnInit call ignores the async, so be careful what you do here:
this.createAndInsertFormfieldComponent();
}
ngOnDestroy(): void {
// Delay the removal to prevent an ExpressionChangedAfterItHasBeenCheckedError:
setTimeout(() => {
this.containerRef?.clear();
if (this.formControl) {
// Reset the form, which also removes the data from this field from the state.
this.formControl.reset();
}
// Remove old FormControl from group
// There is a catch here: Removing the control also cancels the reset event.
// TODO: Nice to have: Find a solution that ensures that the removal is only done
// after the reset is handled everywhere.
// The following ensures that only the control of this component is removed from the group.
// It is possible, that the control was replaced by another one with the same ID in the meantime.
// This syntax "this.group.get([configValue.id])" is needed instead of this one "this.group.get(configValue.id)" because
// otherwise the "get" function will use "." in the ids as a key separator
const configValue = this.config();
if (configValue && this.group && this.group.get([configValue.id]) === this.formControl) {
this.group.removeControl(configValue.id);
}
this.tearDownComponentSubscriptions$.next();
});
}
private checkFormfieldSpacing(spacing: DfFormfieldSpacing): boolean {
const configValue = this.config();
if (configValue?.spacing) {
return configValue.spacing === spacing;
}
return this.defaultSpacing() === spacing;
}
private safelyGetFormGroup(): UntypedFormGroup {
if (!this.group) {
this.group = new UntypedFormGroup({});
}
return this.group;
}
private configureComponent() {
// Stop listening to any previously configured component
this.tearDownComponentSubscriptions$.next();
const componentInstance = this.component?.instance;
this.componentInstance.set(componentInstance);
const configValue = this.config();
if (componentInstance && configValue) {
// Pass down the props
this.component?.setInput('config', configValue);
this.component?.setInput('isRetailChannel', this.isRetailChannel());
this.component?.setInput('formAclPath', this.formAclPath());
// Subscribe to changes to component's AbstractControl
componentInstance.componentOrControlInitFinished
.pipe(takeUntil(this.tearDownComponentSubscriptions$))
.subscribe((control) => {
// Using a setTimeout here prevents an ExpressionChangedAfterItHasBeenCheckedError error.
setTimeout(() => {
this.onFormControlChanged(control);
}, 0);
});
outputToObservable(componentInstance.formEvent)
.pipe(takeUntil(this.tearDownComponentSubscriptions$))
.subscribe((event) => {
this.formEvent.emit(event);
});
}
}
private onFormControlChanged = (formControl: AbstractControl | undefined) => {
const configValue = this.config();
if (configValue?.type === 'LINE_BREAK') {
this.componentOrControlInitFinished.next();
return;
}
if (!configValue?.id) {
return;
}
this.formControl = formControl;
const group = this.safelyGetFormGroup();
if (group.get(configValue.id)) {
throw new Error(
`Dynamic Form: The form control with the ID "${configValue.id}" already exists in the the formGroup.`
);
}
group.setControl(configValue.id, formControl);
this.componentOrControlInitFinished.next();
};
private async createAndInsertFormfieldComponent() {
const configValue = this.config();
if (!configValue?.type) {
return;
}
// Create desired form field component
const { component, moduleRef } = await this.componentService.getComponent(configValue);
this.component = this.containerRef?.createComponent<DfBaseComponent>(
component as unknown as typeof DfBaseComponent,
{
ngModuleRef: moduleRef,
injector: this.injector
}
);
this.configureComponent();
}
}
<ng-template #container></ng-template>
@use '../breakpoints.scss' as *;
:host {
--df-formfield-spacing-desktop-default: var(--vertical-inner-section-spacing);
--df-formfield-spacing-mobile-default: var(--vertical-inner-section-spacing);
&:has(df-headline) {
--df-formfield-spacing-desktop-default: 0px !important;
}
display: block;
::ng-deep {
// Removes NDBX formfield paddings
--formfield-bottom-padding: 0;
--formfield-mobile-bottom-padding: 0;
}
&:not(:empty) {
/**
* Formfield components can override the default spacing by overriding the following tokens:
* --df-formfield-spacing-desktop-default
* --df-formfield-spacing-mobile-default
*/
margin-bottom: var(--df-formfield-spacing-desktop-default);
&.retail {
margin-bottom: var(--df-formfield-spacing-desktop-default);
@media (max-width: $breakpoint-m) {
margin-bottom: var(--df-formfield-spacing-mobile-default);
}
}
&.df-spacing-none {
margin-bottom: var(--df-formfield-spacing-none, 0);
}
&.df-spacing-xs {
margin-bottom: var(--df-formfield-spacing-desktop-xs, 8px);
@media (max-width: $breakpoint-m) {
margin-bottom: var(--df-formfield-spacing-mobile-xs, 8px);
}
}
&.df-spacing-s {
margin-bottom: var(--df-formfield-spacing-desktop-s, 16px);
@media (max-width: $breakpoint-m) {
margin-bottom: var(--df-formfield-spacing-mobile-s, 8px);
}
}
&.df-spacing-m {
margin-bottom: var(--df-formfield-spacing-desktop-m, 24px);
@media (max-width: $breakpoint-m) {
margin-bottom: var(--df-formfield-spacing-mobile-m, 16px);
}
}
&.df-spacing-l {
margin-bottom: var(--df-formfield-spacing-desktop-l, 32px);
@media (max-width: $breakpoint-m) {
margin-bottom: var(--df-formfield-spacing-mobile-l, 24px);
}
}
&.df-spacing-xl {
margin-bottom: var(--df-formfield-spacing-desktop-xl, 40px);
@media (max-width: $breakpoint-m) {
margin-bottom: var(--df-formfield-spacing-mobile-xl, 32px);
}
}
&.df-spacing-xxl {
margin-bottom: var(--df-formfield-spacing-desktop-xxl, 48px);
@media (max-width: $breakpoint-m) {
margin-bottom: var(--df-formfield-spacing-mobile-xxl, 40px);
}
}
}
&:empty {
display: none;
}
}
// Removes last formfield's margin-bottom.
::ng-deep df-form {
df-formfield:last-of-type {
margin-bottom: 0 !important;
}
}
Legend
Html element with directive