libs/core/dynamic-form/standalone/src/lib/dynamic-form/dynamic-form.component.ts

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor()

Inputs

existingFormGroup
Type : FormGroup | 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.

formConfig
Type : DynamicFormConfiguration | undefined
Default value : undefined
formRawValue
Type : Record<string, unknown> | undefined
Default value : undefined

Outputs

businessEvent
Type : string
formEvent
Type : DfEventPayload
formGroupChange
Type : UntypedFormGroup | undefined
formInitFinished
Type : boolean
formRawValue
Type : Record<string, unknown> | undefined
formValid
Type : boolean

Methods

detectFrame
detectFrame()
Returns : void
setValidationConfiguration
setValidationConfiguration(validationRules: ValidationRule[])
Parameters :
Name Type Optional
validationRules ValidationRule[] No
Returns : void

Properties

Readonly containerWidth
Type : unknown
Default value : signal(Infinity)
formConfigWithoutValidators
Type : DynamicFormConfiguration | undefined
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;
    });
  }
}
<df-form
  [id]="id"
  [formConfig]="formConfigWithoutValidators"
  (formInitFinished)="formInitFinished.emit($event)"
  [existingFormGroup]="existingFormGroup()"
  (formGroupChange)="formGroupChange.emit($event)"
  (formValid)="formValid.emit($event)"
  [(formRawValue)]="formRawValue"
  [transformedValidationConfiguration]="transformedValidationConfiguration"
  (formEvent)="processFormEvent($event)"
  data-testid="dynamicForm"
></df-form>

./dynamic-form.component.scss

@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};
          }
        }
      }
    }
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""