libs/dynamic-form-playground-bbs/src/lib/playground-dynamic-form-with-config-editor/dynamic-form-config-editor/dynamic-form-config-editor.component.ts

Metadata

Relationships

Depends on

import { NxButtonComponent } from '@allianz/ng-aquila/button';
import { NxCheckboxComponent } from '@allianz/ng-aquila/checkbox';
import { NxDropdownComponent, NxDropdownItemComponent } from '@allianz/ng-aquila/dropdown';
import { NxFormfieldComponent } from '@allianz/ng-aquila/formfield';
import { NxColComponent, NxLayoutComponent, NxRowComponent } from '@allianz/ng-aquila/grid';
import { NxLinkComponent } from '@allianz/ng-aquila/link';
import { PfeBusinessService } from '@allianz/ngx-pfe';
import {
  formFieldConfigurations,
  fullExampleConfig
} from '@allianz/taly-core/devtools/dynamic-form-editor';
import { DfFormLayoutType } from '@allianz/taly-core/dynamic-form';
import { TalyInternalHeadlineComponent } from '@allianz/taly-core/ui';
import { DebugToolsService } from '@allianz/taly-core/devtools';
import { DfBasicComponentsConfig, DynamicFormConfiguration } from '@allianz/taly-core/schemas';
import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  DestroyRef,
  inject,
  input,
  output
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { MonacoEditorModule, NgxEditorModel } from '@allianz/taly-core/monaco-editor';
import { debounceTime, filter, Subject } from 'rxjs';

@Component({
  selector: 'bc-dynamic-form-config-editor',
  imports: [
    NxColComponent,
    NxRowComponent,
    NxButtonComponent,
    NxLayoutComponent,
    MonacoEditorModule,
    FormsModule,
    NxDropdownItemComponent,
    NxDropdownComponent,
    NxFormfieldComponent,
    NxCheckboxComponent,
    TalyInternalHeadlineComponent,
    NxLinkComponent
  ],
  standalone: true,
  templateUrl: './dynamic-form-config-editor.component.html',
  styleUrl: './dynamic-form-config-editor.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DynamicFormConfigEditorComponent {
  protected DF_LAYOUT_TYPES: string[] = Object.values(DfFormLayoutType) as DfFormLayoutType[];

  formConfig = input<DynamicFormConfiguration>();
  isExpertChannel = input<boolean>();
  formConfigChange = output<DynamicFormConfiguration>();

  protected monacoModel!: NgxEditorModel;
  protected monacoFormConfigText: string | undefined;
  protected layoutType: DfFormLayoutType = DfFormLayoutType.OneColumn;
  protected headlineWidthMatchesLayoutType?: boolean;
  protected paragraphWidthMatchesLayoutType?: boolean;
  protected fullWidthFormInExpert?: boolean;
  protected editorOptions = {
    theme: 'vs-dark',
    language: 'json',
    fontSize: 15
  };
  protected debugToolsVisible = inject(DebugToolsService).debugToolsVisible;
  protected formFields: DfBasicComponentsConfig[] = Object.values(formFieldConfigurations).filter(
    (fieldConfig) => fieldConfig !== null
  );

  private cdr = inject(ChangeDetectorRef);
  private destroyRef = inject(DestroyRef);
  private pfeBusinessService = inject(PfeBusinessService);
  private debugToolsService = inject(DebugToolsService);
  private monacoUpdatedConfig$ = new Subject<DynamicFormConfiguration | undefined>();
  private monacoInitialized = false;

  constructor() {
    this.updateFormConfigLink(this.formConfig());
  }

  protected onMonacoInit() {
    if (this.monacoInitialized) {
      return;
    }

    this.monacoUpdatedConfig$
      .pipe(
        debounceTime(200),
        filter((data): data is NonNullable<typeof data> => data !== undefined),
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe((data) => {
        this.updateFormConfig(data);
      });

    this.cdr.markForCheck();
    this.monacoInitialized = true;

    this.layoutType = this.formConfig()?.layout?.type || DfFormLayoutType.OneColumn;
    this.headlineWidthMatchesLayoutType = this.formConfig()?.layout?.headlineWidthMatchesLayoutType;
    this.paragraphWidthMatchesLayoutType =
      this.formConfig()?.layout?.paragraphWidthMatchesLayoutType;
    this.fullWidthFormInExpert = this.formConfig()?.layout?.fullWidthFormInExpert;

    this.updateFormConfigLink(this.formConfig());
    this.updateMonacoContent(this.formConfig());
  }

  protected monacoChange($event: string) {
    // edge case if the user clears the editor or adds an empty object
    const monacoConfigWithoutWhitespace = $event.replace(/\s+/g, '');
    if (monacoConfigWithoutWhitespace === '' || monacoConfigWithoutWhitespace === '{}') {
      this.monacoUpdatedConfig$.next({ fields: [], layout: { type: DfFormLayoutType.OneColumn } });
      return;
    }

    try {
      const dynamicFormConfiguration = JSON.parse($event);
      if (!dynamicFormConfiguration.fields) {
        dynamicFormConfiguration.fields = [];
      }
      this.monacoUpdatedConfig$.next(dynamicFormConfiguration);
    } catch (error) {
      console.warn(error);
    }
  }

  protected onLayoutTypeChange(layoutType: DfFormLayoutType) {
    const currentConfig = this.formConfig();
    if (currentConfig) {
      const newConfig = structuredClone(currentConfig);
      newConfig.layout = {
        ...currentConfig.layout,
        type: layoutType
      };

      this.updateFormConfig(newConfig);
      this.updateMonacoContent(newConfig);
    }
  }

  protected onHeadlineWidthMatchesLayoutTypeChange(headlineWidthMatchesLayoutType: boolean) {
    const currentConfig = this.formConfig();
    if (currentConfig) {
      const newConfig = structuredClone(currentConfig);
      newConfig.layout = {
        ...(currentConfig.layout || { type: DfFormLayoutType.OneColumn }),
        headlineWidthMatchesLayoutType
      };

      this.updateFormConfig(newConfig);
      this.updateMonacoContent(newConfig);
    }
  }

  protected onParagraphWidthMatchesLayoutTypeChange(paragraphWidthMatchesLayoutType: boolean) {
    const currentConfig = this.formConfig();
    if (currentConfig) {
      const newConfig = structuredClone(currentConfig);
      newConfig.layout = {
        ...(currentConfig.layout || { type: DfFormLayoutType.OneColumn }),
        paragraphWidthMatchesLayoutType
      };

      this.updateFormConfig(newConfig);
      this.updateMonacoContent(newConfig);
    }
  }

  protected onFullWidthFormInExpertChange(fullWidthFormInExpert: boolean) {
    const currentConfig = this.formConfig();
    if (currentConfig) {
      const newConfig = structuredClone(currentConfig);
      newConfig.layout = {
        ...(currentConfig.layout || { type: DfFormLayoutType.OneColumn }),
        fullWidthFormInExpert
      };

      this.updateFormConfig(newConfig);
      this.updateMonacoContent(newConfig);
    }
  }

  protected addFullExample() {
    const fieldId = this.generateFieldId();
    const fullExampleConfigString = JSON.stringify(fullExampleConfig);
    const fullExampleConfigWithoutIndexPlaceholder = fullExampleConfigString.replace(
      /-{talyFieldId}/g,
      `-${fieldId}`
    );
    const fields: DfBasicComponentsConfig[] = JSON.parse(fullExampleConfigWithoutIndexPlaceholder);
    this.addFields(fields, fieldId);
  }

  protected addFields(addedFields: DfBasicComponentsConfig[], fieldId?: string) {
    if (!fieldId) {
      fieldId = this.generateFieldId();
    }
    const fields: DfBasicComponentsConfig[] = structuredClone(addedFields);

    const currentConfig = this.formConfig();
    if (currentConfig) {
      const newConfig = structuredClone(currentConfig);
      fields.forEach((field) => {
        field.id = field.id + `-${fieldId}`;
        newConfig.fields.push(field);
      });

      this.updateFormConfig(newConfig);
      this.updateMonacoContent(newConfig);
    }
  }

  private updateFormConfig(newFormConfig: DynamicFormConfiguration) {
    this.layoutType = newFormConfig.layout?.type || DfFormLayoutType.OneColumn;
    this.headlineWidthMatchesLayoutType = newFormConfig.layout?.headlineWidthMatchesLayoutType;
    this.paragraphWidthMatchesLayoutType = newFormConfig.layout?.paragraphWidthMatchesLayoutType;
    this.fullWidthFormInExpert = newFormConfig.layout?.fullWidthFormInExpert;

    this.formConfigChange.emit(newFormConfig);
    this.updateFormConfigLink(newFormConfig);

    this.cdr.markForCheck();
  }

  private updateMonacoContent(formConfig?: DynamicFormConfiguration) {
    this.monacoFormConfigText = JSON.stringify(formConfig, null, 2);

    this.monacoModel = {
      value: this.monacoFormConfigText || '',
      language: 'json',
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      uri: (window as any).monaco?.Uri?.parse
        ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
          (window as any).monaco.Uri.parse(
            `a://${Math.random().toString(36).substring(7)}/dynamic-form.json`
          )
        : undefined
    };

    this.cdr.markForCheck();
  }

  private updateFormConfigLink(formConfig?: DynamicFormConfiguration) {
    this.debugToolsService.encodedDynamicFormConfig.set(
      encodeURIComponent(JSON.stringify(formConfig || {}))
    );

    if (formConfig) {
      this.pfeBusinessService.storeValueByExpression('$.formConfigFromEditor', formConfig);
    }
  }

  private generateFieldId() {
    return Math.random().toString(36).substring(7);
  }
}
@if (this.debugToolsVisible()) {
<div class="config-editor-container" nxLayout="grid nopadding" [containerQuery]="true">
  <hr />
  <div nxRow [rowJustify]="isExpertChannel() ? 'start' : 'center'">
    <div [nxCol]="isExpertChannel() ? '12,12,6' : '12,12,8'">
      <taly-internal-headline type="subsection">Basic Form Configuration</taly-internal-headline>
      <h4>Form Field Configuration:</h4>
      <p>
        Use the buttons below to add form components and adjust the layout of your Dynamic Form.
      </p>
      <div class="buttons">
        @for (field of formFields; track field.id) {
        <button
          nxButton="secondary small"
          type="button"
          (click)="addFields([field])"
          [attr.data-testid]="`add-${field.id}-button`"
        >
          {{ `Add ${field.label}` }}
        </button>
        }
        <button
          nxButton="primary small"
          type="button"
          (click)="addFullExample()"
          data-testid="add-full-example-button"
        >
          Add Full Example Form
        </button>
      </div>

      <h4>Form Layout Configuration:</h4>
      <p>
        The options below allow you to customize the appearance of the entire form. Take a look at
        our
        <nx-link nxStyle="text"
          ><a
            href="https://taly.frameworks.allianz.io/additional-documentation/app-configuration/individual-page-configuration.html#layout"
            target="”_blank”"
            >documentation</a
          ></nx-link
        >
        to learn more about these properties.
      </p>
      <nx-formfield label="Layout Type">
        <nx-dropdown
          (valueChange)="onLayoutTypeChange($event)"
          [value]="layoutType"
          data-testid="layout-type-dropdown"
          [attr.data-selected-layout]="layoutType"
        >
          @for (layoutType of DF_LAYOUT_TYPES; track layoutType) {
          <nx-dropdown-item [value]="layoutType">{{ layoutType }}</nx-dropdown-item>
          }
        </nx-dropdown>
      </nx-formfield>
      <nx-checkbox
        class="nx-margin-bottom-s"
        (checkedChange)="onHeadlineWidthMatchesLayoutTypeChange($event)"
        [checked]="headlineWidthMatchesLayoutType"
        data-testid="headline-width-checkbox"
      >
        headlineWidthMatchesLayoutType
      </nx-checkbox>
      <nx-checkbox
        class="nx-margin-bottom-s"
        (checkedChange)="onParagraphWidthMatchesLayoutTypeChange($event)"
        [checked]="paragraphWidthMatchesLayoutType"
        data-testid="paragraph-width-checkbox"
      >
        paragraphWidthMatchesLayoutType
      </nx-checkbox>
      @if (isExpertChannel()){
      <nx-checkbox
        class="nx-margin-bottom-s"
        (checkedChange)="onFullWidthFormInExpertChange($event)"
        [checked]="fullWidthFormInExpert"
        data-testid="expert-full-width-checkbox"
      >
        fullWidthFormInExpert
      </nx-checkbox>
      }

      <div class="monaco-section">
        <taly-internal-headline type="subsection">Configuration Editor</taly-internal-headline>
        <div>
          Change this configuration to update the form live. The editor has json schema support,
          which can be triggered via <kbd>Ctrl/Cmd</kbd>+<kbd>Space</kbd> or
          <kbd>Ctrl/Cmd</kbd>+<kbd>i</kbd>
        </div>
        <taly-monaco-editor
          data-testid="monaco-editor"
          class="monaco-editor"
          [options]="editorOptions"
          [model]="monacoModel"
          (onInit)="onMonacoInit()"
          [(ngModel)]="monacoFormConfigText"
          (ngModelChange)="monacoChange($event)"
          [attr.data-monaco-text]="monacoFormConfigText"
        ></taly-monaco-editor>
      </div>
    </div>
  </div>
</div>
}

./dynamic-form-config-editor.component.scss

.config-editor-container {
  margin-top: var(--vertical-outer-section-spacing);
}

.buttons {
  display: flex;
  flex-wrap: wrap;
  margin: -4px;
  width: 100%;
  justify-content: flex-start;

  button {
    margin: 0 4px 16px;
    flex: 1 1 0;
    min-width: 196px;
  }
}

textarea {
  height: 300px;
  width: 100%;
  max-width: 100%;
}

h4 {
  margin-bottom: 8px;
}

.monaco-editor {
  height: 600px;
}

.monaco-section {
  margin-top: var(--vertical-inner-section-spacing);
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""