import {
CHANNEL,
CHANNEL_TOKEN,
USE_ENHANCED_VERTICAL_SPACING,
ValidationConfigItem,
ValidationRule
} from '@allianz/taly-core';
import {
CUSTOM_COMPONENT_TYPE,
DfDynamicValidatorService,
type DfEventPayload,
DfFormModule,
DfInteractiveEventConfig,
FORM_CONTAINER_WIDTH
} from '@allianz/taly-core/dynamic-form';
import { TALY_FRAME_ELEMENT } from '@allianz/taly-core/frame';
import { type DfBasicComponentsConfig, DynamicFormConfiguration } from '@allianz/taly-core/schemas';
import {
afterNextRender,
ChangeDetectionStrategy,
Component,
DestroyRef,
effect,
ElementRef,
inject,
input,
model,
output,
signal
} from '@angular/core';
import { FormGroup, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
@Component({
selector: 'taly-dynamic-form',
imports: [ReactiveFormsModule, DfFormModule],
templateUrl: './dynamic-form.component.html',
styleUrl: './dynamic-form.component.scss',
host: {
'[class.is-retail]': 'isRetailChannel',
'[class.use-enhanced-vertical-spacing]': 'useEnhancedVerticalSpacing',
'[class.has-frame]': 'hasFrame()'
},
providers: [
{
provide: FORM_CONTAINER_WIDTH,
useFactory: (component: TalyDynamicFormComponent) => component.containerWidth,
deps: [TalyDynamicFormComponent]
}
],
// This component wraps df-form which can potentially render user-made form fields.
// It must use Eager change detection so that those components are checked properly.
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
changeDetection: ChangeDetectionStrategy.Eager
})
/**
* This experimental component provides a way to use the Dynamic Form standalone.
*/
export class TalyDynamicFormComponent {
private channel = inject(CHANNEL_TOKEN);
protected isRetailChannel = this.channel === CHANNEL.RETAIL;
protected useEnhancedVerticalSpacing = inject(USE_ENHANCED_VERTICAL_SPACING);
private frameElement = inject(TALY_FRAME_ELEMENT, { optional: true });
private elementRef = inject(ElementRef);
private destroyRef = inject(DestroyRef);
protected hasFrame = signal(false);
readonly containerWidth = signal(Infinity);
formConfig = input<DynamicFormConfiguration | undefined>(undefined);
formConfigWithoutValidators: DynamicFormConfiguration | undefined;
/**
* This optional input allows it to hand over an existing formGroup that the Dynamic Form should use.
* It will then add the formControls of the fields directly to this formGroup.
*/
existingFormGroup = input<FormGroup | undefined>();
formInitFinished = output<boolean>();
formValid = output<boolean>();
formEvent = output<DfEventPayload>();
businessEvent = output<string>();
formRawValue = model<Record<string, unknown> | undefined>(undefined);
formGroupChange = output<UntypedFormGroup | undefined>();
protected id = Math.random().toString(36).slice(2, 7);
protected transformedValidationConfiguration: ValidationConfigItem[] | undefined;
private dfDynamicValidatorService = inject(DfDynamicValidatorService, { optional: true });
constructor() {
effect(() => {
const formConfigValue = this.formConfig();
if (formConfigValue?.fields) {
const fieldConfigs = formConfigValue.fields;
const validators = this.getValidators(fieldConfigs);
const fieldsWithoutValidators = this.removeValidatorsFromFields(fieldConfigs);
this.formConfigWithoutValidators = { ...formConfigValue, fields: fieldsWithoutValidators };
this.setValidationConfiguration(validators);
}
});
afterNextRender(() => {
this.detectFrame();
this.observeContainerWidth();
});
}
private observeContainerWidth(): void {
const hostElement = this.elementRef.nativeElement as HTMLElement;
this.containerWidth.set(hostElement.offsetWidth);
const resizeObserver = new ResizeObserver((entries) => {
this.containerWidth.set(entries[0].contentRect.width);
});
resizeObserver.observe(hostElement);
this.destroyRef.onDestroy(() => resizeObserver.disconnect());
}
detectFrame(): void {
this.hasFrame.set(
!!this.frameElement && this.frameElement.contains(this.elementRef.nativeElement)
);
}
setValidationConfiguration(validationRules: ValidationRule[]) {
if (this.dfDynamicValidatorService) {
this.transformedValidationConfiguration =
this.dfDynamicValidatorService.convertDynamicValidatorConfigToObservable(validationRules);
} else {
console.warn(
'Dynamic validator functionality is unavailable because DfDynamicValidatorService is not provided. Implement and provide this service to enable dynamic validation.'
);
this.transformedValidationConfiguration = validationRules as ValidationConfigItem[];
}
}
protected processFormEvent(event: DfEventPayload) {
// We proxy the event to the standalone component consumer
this.formEvent.emit(event);
// We check if there is a business event associated and propagate it to the standalone component consumer
const formConfigValue = this.formConfig();
const fieldConfig = formConfigValue?.fields.find(
(field) => field.id === event.fieldIdConfig
) as DfBasicComponentsConfig;
let eventConfig: DfInteractiveEventConfig | undefined;
if (fieldConfig.type === CUSTOM_COMPONENT_TYPE) {
eventConfig = fieldConfig.config?.[event.type] as DfInteractiveEventConfig | undefined;
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
eventConfig = (fieldConfig as any)[event.type];
}
if (eventConfig) {
this.businessEvent.emit(`${event.fieldIdConfig}.${event.type}`);
}
}
private getValidators(fields: DfBasicComponentsConfig[]): ValidationRule[] {
return fields.reduce((acc: ValidationRule[], field: DfBasicComponentsConfig) => {
if ('validators' in field && field?.validators) {
const validatorWithFieldId = field.validators.map((validator) => {
return { id: field.id, ...validator };
});
acc.push(...validatorWithFieldId);
}
return acc;
}, []);
}
private removeValidatorsFromFields(fields: DfBasicComponentsConfig[]): DfBasicComponentsConfig[] {
return fields.map((field) => {
if ('validators' in field && field?.validators) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { validators, ...rest } = field;
return rest;
}
return field;
});
}
}
@use '../../../../../frame/styles/vertical-spacing-tokens.scss' as *;
:host {
display: block;
container-type: inline-size;
&:not(.has-frame) {
// Fallback values for when frame is not present (e.g., overlays, standalone)
// Expert channel
--vertical-outer-section-spacing: #{$vertical-outer-section-spacing};
--vertical-outer-sub-section-spacing: #{$vertical-outer-sub-section-spacing};
--vertical-layout-helper: #{$vertical-layout-helper};
--vertical-helper-headline-spacing: #{$vertical-helper-headline-spacing};
--vertical-inner-section-spacing: #{$vertical-inner-section-spacing};
--vertical-helper-high-spacing: #{$vertical-helper-high-spacing};
--vertical-helper-low-spacing: #{$vertical-helper-low-spacing};
&.use-enhanced-vertical-spacing {
--vertical-headline-helper: #{$vertical-headline-helper};
--vertical-section-inner: #{$vertical-section-inner};
}
// Retail channel
&.is-retail {
--vertical-outer-section-spacing: #{$vertical-outer-section-spacing-retail};
--vertical-outer-sub-section-spacing: #{$vertical-outer-sub-section-spacing-retail};
--vertical-layout-helper: #{$vertical-layout-helper-retail};
--vertical-helper-headline-spacing: #{$vertical-helper-headline-spacing-retail};
--vertical-inner-section-spacing: #{$vertical-inner-section-spacing-retail};
--vertical-helper-high-spacing: #{$vertical-helper-high-spacing-retail};
--vertical-helper-low-spacing: #{$vertical-helper-low-spacing-retail};
df-form {
@container (max-width: 704px) {
--vertical-outer-section-spacing: #{$vertical-outer-section-spacing-retail-mobile};
--vertical-outer-sub-section-spacing: #{$vertical-outer-sub-section-spacing-retail-mobile};
--vertical-layout-helper: #{$vertical-layout-helper-retail-mobile};
--vertical-helper-headline-spacing: #{$vertical-helper-headline-spacing-retail-mobile};
--vertical-inner-section-spacing: #{$vertical-inner-section-spacing-retail-mobile};
--vertical-helper-high-spacing: #{$vertical-helper-high-spacing-retail-mobile};
--vertical-helper-low-spacing: #{$vertical-helper-low-spacing-retail-mobile};
}
}
&.use-enhanced-vertical-spacing {
--vertical-outer-section-spacing: #{$vertical-outer-section-spacing-enhanced-retail};
--vertical-headline-helper: #{$vertical-headline-helper-retail};
--vertical-section-inner: #{$vertical-section-inner-retail};
df-form {
@container (max-width: 704px) {
--vertical-headline-helper: #{$vertical-headline-helper-enhanced-mobile};
--vertical-section-inner: #{$vertical-section-inner-enhanced-mobile};
--vertical-outer-section-spacing: #{$vertical-outer-section-spacing-enhanced-mobile};
--vertical-outer-sub-section-spacing: #{$vertical-outer-sub-section-spacing-enhanced-mobile};
--vertical-helper-headline-spacing: #{$vertical-helper-headline-spacing-enhanced-mobile};
--vertical-inner-section-spacing: #{$vertical-inner-section-spacing-enhanced-mobile};
--vertical-helper-low-spacing: #{$vertical-helper-low-spacing-enhanced-mobile};
}
}
}
}
}
}