libs/core/devtools/dynamic-form-editor/src/lib/dynamic-form-editor/dynamic-form-editor.component.ts

Implements

OnInit OnDestroy AfterViewInit

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods

Constructor

constructor()

Methods

addFormfield
addFormfield(formfieldType: string, index?: number)
Parameters :
Name Type Optional
formfieldType string No
index number Yes
Returns : void
cancelConfirmationDialog
cancelConfirmationDialog()
Returns : void
changeLayoutType
changeLayoutType(layout: DfFormLayoutType)
Parameters :
Name Type Optional
layout DfFormLayoutType No
Returns : void
closeModal
closeModal()
Returns : void
closeSaveResultDialog
closeSaveResultDialog()
Returns : void
confirmationDialogDiscardChanges
confirmationDialogDiscardChanges()
Returns : void
discardChanges
discardChanges()
Returns : void
moveFormfield
moveFormfield(previousIndex: number, currentIndex: number)
Parameters :
Name Type Optional
previousIndex number No
currentIndex number No
Returns : void
onFormConfigErrors
onFormConfigErrors(hasErrors: boolean)
Parameters :
Name Type Optional
hasErrors boolean No
Returns : void
onFullWidthFormInExpertChange
onFullWidthFormInExpertChange(checked: boolean)
Parameters :
Name Type Optional
checked boolean No
Returns : void
onHeadlineWidthMatchesLayoutTypeChange
onHeadlineWidthMatchesLayoutTypeChange(checked: boolean)
Parameters :
Name Type Optional
checked boolean No
Returns : void
onParagraphWidthMatchesLayoutTypeChange
onParagraphWidthMatchesLayoutTypeChange(checked: boolean)
Parameters :
Name Type Optional
checked boolean No
Returns : void
onTabIndexChange
onTabIndexChange(index: number)
Parameters :
Name Type Optional
index number No
Returns : void
saveChanges
saveChanges()
Returns : void
setNewConfigFromEditor
setNewConfigFromEditor(newConfig: DynamicFormConfiguration)
Parameters :
Name Type Optional
newConfig DynamicFormConfiguration No
Returns : void

Properties

appRootElementRef
Type : unknown
Default value : inject(APP_ROOT_ELEMENT_REF)
confirmationTemplateRef
Type : unknown
Default value : viewChild<TemplateRef<unknown>>('confirmationTemplate')
dfMonacoDiffEditorContainer
Type : unknown
Default value : viewChild< TemplateRef<ElementRef<DynamicFormMonacoEditorComponent>> >('dfMonacoDiffEditorContainer')
dfMonacoEditorContainer
Type : unknown
Default value : viewChild<TemplateRef<ElementRef<DynamicFormMonacoEditorComponent>>>('dfMonacoEditorContainer')
editorInfo
Type : unknown
Default value : EDITOR_INFO
editorTabIndex
Type : number
Default value : 0
formConfig
Type : WritableSignal<DynamicFormConfiguration>
formfieldTypes
Type : string[]
Default value : Object.entries(formFieldConfigurations) .filter(([, config]) => config !== null) .map(([type]) => type)
formGroup
Type : unknown
Default value : new UntypedFormGroup({})
fullWidthFormInExpert
Type : unknown
Default value : computed(() => this.formConfig().layout?.fullWidthFormInExpert)
hasChanges
Type : unknown
Default value : computed(() => !isEqual(this.initialFormConfig, this.formConfig()))
headlineWidthMatchesLayoutType
Type : unknown
Default value : computed( () => this.formConfig().layout?.headlineWidthMatchesLayoutType )
highlightedPosition
Type : unknown
Default value : signal<number | undefined>(undefined)
initialFormConfig
Type : DynamicFormConfiguration
isAddFieldModeActive
Type : unknown
Default value : false
isConfigValid
Type : unknown
Default value : true
isRearrangeModeActive
Type : unknown
Default value : false
isServerAvailable
Type : unknown
Default value : signal(false)
layoutOptions
Type : unknown
Default value : Object.entries(DfFormLayoutType).map(([label, value]) => ({ label, value }))
paragraphWidthMatchesLayoutType
Type : unknown
Default value : computed( () => this.formConfig().layout?.paragraphWidthMatchesLayoutType )
placeholderGridColumns
Type : unknown
Default value : computed(() => { const layoutType = this.selectedLayoutType(); switch (layoutType) { case DfFormLayoutType.TwoColumn: return 6; case DfFormLayoutType.ThreeColumn: return 4; case DfFormLayoutType.FourColumn: return 3; default: return 12; } })
popoverLayout
Type : unknown
Default value : viewChild.required<NxPopoverComponent>('popoverLayout')
saveResultIsError
Type : unknown
Default value : signal(false)
saveResultMessage
Type : unknown
Default value : signal('')
saveResultTemplateRef
Type : unknown
Default value : viewChild<TemplateRef<unknown>>('saveResultTemplate')
selectedLayoutType
Type : unknown
Default value : computed(() => this.formConfig().layout?.type)
transformedValidationConfiguration
Type : ValidationConfigItem[] | undefined
import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  inject,
  signal,
  TemplateRef,
  viewChild,
  OnInit,
  ElementRef,
  Renderer2,
  OnDestroy,
  AfterViewInit,
  computed,
  WritableSignal,
  DestroyRef
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule, UntypedFormGroup } from '@angular/forms';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { MonacoEditorModule } from '@allianz/taly-core/monaco-editor';
import { NgxJsonViewerModule } from 'ngx-json-viewer';
import isEqual from 'lodash-es/isEqual';
import {
  DfDynamicValidatorService,
  DfFormLayoutType,
  DfInfoIconComponent,
  type DfInfoIconConfig
} from '@allianz/taly-core/dynamic-form';
import { NxPopoverComponent, NxPopoverTriggerDirective } from '@allianz/ng-aquila/popover';
import {
  NX_MODAL_DATA,
  NxDialogService,
  NxModalContentDirective,
  NxModalActionsDirective,
  NxModalRef
} from '@allianz/ng-aquila/modal';
import { NxToolbarComponent, NxToolbarDividerComponent } from '@allianz/ng-aquila/toolbar';
import { NxSwitcherComponent } from '@allianz/ng-aquila/switcher';
import { NxButtonComponent, NxPlainButtonComponent } from '@allianz/ng-aquila/button';
import { NxFormfieldComponent } from '@allianz/ng-aquila/formfield';
import { NxDropdownComponent, NxDropdownItemComponent } from '@allianz/ng-aquila/dropdown';
import {
  NxSidepanelComponent,
  NxSidepanelContentComponent,
  NxSidepanelHeaderComponent,
  NxSidepanelOuterContainerComponent
} from '@allianz/ng-aquila/sidepanel';
import { NxTabComponent, NxTabGroupComponent } from '@allianz/ng-aquila/tabs';
import { NxHeadlineComponent } from '@allianz/ng-aquila/headline';
import { catchError, map, of, take } from 'rxjs';
import { DynamicFormPreviewComponent } from './dynamic-form-preview/dynamic-form-preview.component';
import { DynamicFormMonacoEditorComponent } from './dynamic-form-monaco-editor/dynamic-form-monaco-editor.component';
import { NxIconComponent, NxStatusIconComponent } from '@allianz/ng-aquila/icon';
import { DfBasicComponentsConfig, DynamicFormConfiguration } from '@allianz/taly-core/schemas';
import { DfDebuggerContext } from '@allianz/taly-core/devtools';
import { formFieldConfigurations } from '../form-field-configurations';
import { provideDynamicFormNativeComponents } from '@allianz/taly-core/dynamic-form/standalone';
import { ResizablePanelsComponent } from './resizable-panels/resizable-panels.component';
import { NxCheckboxComponent } from '@allianz/ng-aquila/checkbox';
import {
  APP_ROOT_ELEMENT_REF,
  IS_A1,
  ValidationConfigItem,
  ValidationRule
} from '@allianz/taly-core';
import { OverlayContainer } from '@angular/cdk/overlay';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

const EDITOR_INFO: DfInfoIconConfig = {
  popoverText:
    '**Welcome to the Dynamic Form Editor!**<br>' +
    'The **Dynamic Form Editor** allows you to modify dynamic form configurations. ' +
    'You can add, remove, and rearrange form fields, change the layout and edit the JSON configuration directly.<br>' +
    'The configuration editor support **JSON schema** assistance, which can be triggered using _Ctrl_/_Cmd_ + _Space_ or _Ctrl_/_Cmd_ + _i_. ' +
    'You can check your changes in the **Diff** tab and the state status in the **State** tab.<br>' +
    'Feel free to take a look at our documentation about the dynamic form and its components [here](https://taly.frameworks.allianz.io/additional-documentation/dynamic-form.html)',
  popoverDirection: 'bottom'
};

@Component({
  selector: 'taly-dynamic-form-editor',
  templateUrl: './dynamic-form-editor.component.html',
  styleUrls: ['./dynamic-form-editor.component.scss'],
  imports: [
    MonacoEditorModule,
    FormsModule,
    DragDropModule,
    NgxJsonViewerModule,
    NxToolbarComponent,
    NxToolbarDividerComponent,
    NxSwitcherComponent,
    NxPlainButtonComponent,
    NxPopoverTriggerDirective,
    NxPopoverComponent,
    NxFormfieldComponent,
    NxDropdownComponent,
    NxDropdownItemComponent,
    NxSidepanelOuterContainerComponent,
    NxSidepanelHeaderComponent,
    NxSidepanelContentComponent,
    NxButtonComponent,
    NxTabGroupComponent,
    NxTabComponent,
    NxHeadlineComponent,
    NxModalActionsDirective,
    NxModalContentDirective,
    DynamicFormPreviewComponent,
    DynamicFormMonacoEditorComponent,
    NxSidepanelComponent,
    NxIconComponent,
    NxStatusIconComponent,
    ResizablePanelsComponent,
    NxCheckboxComponent,
    DfInfoIconComponent
  ],
  host: {
    '[class.is-a1]': 'isA1'
  },
  providers: [provideDynamicFormNativeComponents()],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DynamicFormEditorComponent implements OnInit, OnDestroy, AfterViewInit {
  highlightedPosition = signal<number | undefined>(undefined);
  popoverLayout = viewChild.required<NxPopoverComponent>('popoverLayout');
  layoutOptions = Object.entries(DfFormLayoutType).map(([label, value]) => ({
    label,
    value
  }));
  confirmationTemplateRef = viewChild<TemplateRef<unknown>>('confirmationTemplate');
  saveResultTemplateRef = viewChild<TemplateRef<unknown>>('saveResultTemplate');
  isServerAvailable = signal(false);
  hasChanges = computed(() => !isEqual(this.initialFormConfig, this.formConfig()));
  saveResultMessage = signal('');
  saveResultIsError = signal(false);
  dfMonacoEditorContainer =
    viewChild<TemplateRef<ElementRef<DynamicFormMonacoEditorComponent>>>('dfMonacoEditorContainer');
  dfMonacoDiffEditorContainer = viewChild<
    TemplateRef<ElementRef<DynamicFormMonacoEditorComponent>>
  >('dfMonacoDiffEditorContainer');

  formfieldTypes: string[] = Object.entries(formFieldConfigurations)
    .filter(([, config]) => config !== null)
    .map(([type]) => type);
  initialFormConfig: DynamicFormConfiguration;
  formConfig: WritableSignal<DynamicFormConfiguration>;
  isRearrangeModeActive = false;
  isAddFieldModeActive = false;
  isConfigValid = true;
  formGroup = new UntypedFormGroup({});
  editorTabIndex = 0;
  selectedLayoutType = computed(() => this.formConfig().layout?.type);
  headlineWidthMatchesLayoutType = computed(
    () => this.formConfig().layout?.headlineWidthMatchesLayoutType
  );
  paragraphWidthMatchesLayoutType = computed(
    () => this.formConfig().layout?.paragraphWidthMatchesLayoutType
  );
  fullWidthFormInExpert = computed(() => this.formConfig().layout?.fullWidthFormInExpert);
  transformedValidationConfiguration: ValidationConfigItem[] | undefined;
  appRootElementRef = inject(APP_ROOT_ELEMENT_REF);
  editorInfo = EDITOR_INFO;
  placeholderGridColumns = computed(() => {
    const layoutType = this.selectedLayoutType();
    switch (layoutType) {
      case DfFormLayoutType.TwoColumn:
        return 6;
      case DfFormLayoutType.ThreeColumn:
        return 4;
      case DfFormLayoutType.FourColumn:
        return 3;
      default:
        return 12;
    }
  });

  private renderer = inject(Renderer2);
  private cdkParentElement: Element | null | undefined;
  private dialogService = inject(NxDialogService);
  private editorModalRef = inject(NxModalRef);
  private confirmationDialogRef?: NxModalRef<unknown>;
  private saveResultDialogRef?: NxModalRef<unknown>;
  private buildingBlockId?: string;
  private pageId?: string;
  private context: DfDebuggerContext;
  private dfEditorServerBaseUrl?: string;
  private cdr = inject(ChangeDetectorRef);
  private tabGroupComponent = viewChild(NxTabGroupComponent);
  private dfDynamicValidatorService = inject(DfDynamicValidatorService, { optional: true });
  private overlayContainer = inject(OverlayContainer);
  private destroyRef = inject(DestroyRef);
  private http = inject(HttpClient);
  protected isA1 = inject(IS_A1);

  constructor() {
    const { formConfig, buildingBlockId, pageId, context, dfEditorServerPort } =
      inject(NX_MODAL_DATA);
    this.initialFormConfig = formConfig;
    this.formConfig = signal(formConfig || {});
    this.buildingBlockId = buildingBlockId;
    this.pageId = pageId;
    this.context = context;
    if (dfEditorServerPort) {
      this.dfEditorServerBaseUrl = `http://localhost:${dfEditorServerPort}`;
    }
    this.setValidationOptions();
  }

  ngOnInit(): void {
    const tabGroupInstance = this.tabGroupComponent();
    if (tabGroupInstance) {
      tabGroupInstance.mobileAccordion = false;
    }

    if (!this.dfEditorServerBaseUrl) {
      this.isServerAvailable.set(false);
      this.cdr.detectChanges();
      return;
    }

    this.http
      .get<{ status: string }>(`${this.dfEditorServerBaseUrl}/api/ping`)
      .pipe(
        take(1),
        map((response) => response.status === 'ok'),
        catchError(() => of(false)),
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe((available) => {
        this.isServerAvailable.set(available && !!this.buildingBlockId);
        this.cdr.detectChanges();
      });
  }

  ngAfterViewInit(): void {
    if (this.appRootElementRef.shadowRoot) {
      this.cdkParentElement = (
        this.overlayContainer.getContainerElement().getRootNode() as ShadowRoot
      ).host;
      // The regular monaco editor is shown first
      this.renderer.appendChild(
        this.cdkParentElement,
        this.dfMonacoEditorContainer()?.elementRef.nativeElement
      );
    }
  }

  ngOnDestroy(): void {
    if (this.appRootElementRef.shadowRoot && this.cdkParentElement) {
      if (this.dfMonacoEditorContainer()?.elementRef?.nativeElement) {
        this.renderer.removeChild(
          this.cdkParentElement,
          this.dfMonacoEditorContainer()?.elementRef.nativeElement
        );
      }
      if (this.dfMonacoDiffEditorContainer()?.elementRef?.nativeElement) {
        this.renderer.removeChild(
          this.cdkParentElement,
          this.dfMonacoDiffEditorContainer()?.elementRef.nativeElement
        );
      }
    }
  }

  onFormConfigErrors(hasErrors: boolean) {
    this.isConfigValid = !hasErrors;
    if (hasErrors) {
      this.isRearrangeModeActive = false;
      this.isAddFieldModeActive = false;
    }
    this.cdr.detectChanges();
  }

  saveChanges() {
    if (!this.buildingBlockId) {
      throw new Error('Cannot save dynamic form configuration. Missing buildingBlockId.');
    }
    if (this.context === 'pageBlock' && !this.pageId) {
      throw new Error('Cannot save dynamic form configuration. Missing pageId.');
    }

    const payload: { buildingBlockId: string; config: DynamicFormConfiguration; pageId?: string } =
      {
        buildingBlockId: this.buildingBlockId,
        config: this.formConfig()
      };
    if (this.pageId) {
      payload.pageId = this.pageId;
    }

    this.http
      .post(`${this.dfEditorServerBaseUrl}/api/dynamic-form`, payload)
      .pipe(take(1), takeUntilDestroyed(this.destroyRef))
      .subscribe({
        next: () => {
          this.saveResultIsError.set(false);
          this.saveResultMessage.set(
            'Changes saved successfully. The application will reload shortly.'
          );
          this.openSaveResultDialog();
        },
        error: (err) => {
          this.saveResultIsError.set(true);
          this.saveResultMessage.set(
            err?.error?.error || err?.message || 'An unknown error occurred while saving.'
          );
          this.openSaveResultDialog();
        }
      });
  }

  discardChanges() {
    if (this.hasChanges()) {
      const templateRef = this.confirmationTemplateRef();
      if (templateRef) {
        this.confirmationDialogRef = this.dialogService.open(templateRef, {
          disableClose: true
        });
      }
    } else {
      this.editorModalRef.close();
    }
  }

  confirmationDialogDiscardChanges() {
    this.confirmationDialogRef?.close();
    this.editorModalRef.close();
  }

  cancelConfirmationDialog() {
    this.confirmationDialogRef?.close();
  }

  closeModal() {
    this.editorModalRef.close();
  }

  closeSaveResultDialog() {
    this.saveResultDialogRef?.close();
  }

  moveFormfield(previousIndex: number, currentIndex: number) {
    const formConfig = structuredClone(this.formConfig());
    if (formConfig) {
      const movedFormfield = formConfig.fields.splice(previousIndex, 1)[0];
      formConfig.fields.splice(currentIndex, 0, movedFormfield);
      this.formConfig.set(formConfig);
    }
  }

  addFormfield(formfieldType: string, index?: number) {
    const formfieldConfig =
      formFieldConfigurations[formfieldType as keyof typeof formFieldConfigurations];
    if (!formfieldConfig) {
      return;
    }
    const clonedFormfieldConfig = structuredClone(formfieldConfig);
    if (!clonedFormfieldConfig) {
      return;
    }
    clonedFormfieldConfig.id = `${clonedFormfieldConfig.id}-${Math.random()
      .toString(36)
      .substring(2, 7)}`;
    const formConfig = structuredClone(this.formConfig());
    if (formConfig) {
      if (!formConfig.fields) {
        formConfig.fields = [];
      }
      if (index !== undefined) {
        formConfig.fields.splice(index, 0, clonedFormfieldConfig as DfBasicComponentsConfig);
      } else {
        formConfig.fields.push(clonedFormfieldConfig as DfBasicComponentsConfig);
      }
      this.formConfig.set(formConfig);
    }
  }

  changeLayoutType(layout: DfFormLayoutType) {
    const formConfig = structuredClone(this.formConfig());
    formConfig.layout = {
      ...formConfig.layout,
      type: layout
    };
    this.formConfig.set(formConfig);
  }

  onHeadlineWidthMatchesLayoutTypeChange(checked: boolean) {
    const formConfig = structuredClone(this.formConfig());
    formConfig.layout = {
      ...(formConfig.layout || { type: DfFormLayoutType.OneColumn }),
      headlineWidthMatchesLayoutType: checked
    };
    this.formConfig.set(formConfig);
  }

  onParagraphWidthMatchesLayoutTypeChange(checked: boolean) {
    const formConfig = structuredClone(this.formConfig());
    formConfig.layout = {
      ...(formConfig.layout || { type: DfFormLayoutType.OneColumn }),
      paragraphWidthMatchesLayoutType: checked
    };
    this.formConfig.set(formConfig);
  }

  onFullWidthFormInExpertChange(checked: boolean) {
    const formConfig = structuredClone(this.formConfig());
    formConfig.layout = {
      ...(formConfig.layout || { type: DfFormLayoutType.OneColumn }),
      fullWidthFormInExpert: checked
    };
    this.formConfig.set(formConfig);
  }

  setNewConfigFromEditor(newConfig: DynamicFormConfiguration) {
    const configValueFromEditor = JSON.stringify(newConfig);
    const configValue = JSON.stringify(this.formConfig());
    if (configValue !== configValueFromEditor) {
      this.formConfig.set(newConfig);
      this.setValidationOptions();
    }
  }

  onTabIndexChange(index: number) {
    if (this.appRootElementRef.shadowRoot) {
      // Timeout needed to ensure the tab content is rendered before appending the editors
      setTimeout(() => {
        if (index === 0) {
          this.renderer.appendChild(
            this.cdkParentElement,
            this.dfMonacoEditorContainer()?.elementRef.nativeElement
          );
        } else if (index === 1) {
          this.renderer.appendChild(
            this.cdkParentElement,
            this.dfMonacoDiffEditorContainer()?.elementRef.nativeElement
          );
        }
      });
    }
  }

  private openSaveResultDialog() {
    const templateRef = this.saveResultTemplateRef();
    if (templateRef) {
      this.saveResultDialogRef = this.dialogService.open(templateRef, {
        disableClose: true
      });
    }
  }

  private setValidationOptions() {
    if (this.formConfig()?.fields) {
      const validators = this.getValidators(this.formConfig().fields);
      if (this.dfDynamicValidatorService) {
        this.transformedValidationConfiguration =
          this.dfDynamicValidatorService.convertDynamicValidatorConfigToObservable(validators);
      } else {
        console.warn(
          'Dynamic validator functionality is unavailable because DfDynamicValidatorService is not provided. Implement and provide this service to enable dynamic validation.'
        );
        this.transformedValidationConfiguration = validators as ValidationConfigItem[];
      }
      this.cdr.markForCheck();
    }
  }

  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;
    }, []);
  }
}
<div class="form-editor" nxModalContent>
  <taly-resizable-panels [initialLeftRatio]="1" [initialRightRatio]="1">
    <div class="form" left>
      <nx-toolbar class="form-layout-toolbar">
        <h3>DF Editor</h3>
        <df-info-icon class="nx-margin-left-xs" [config]="editorInfo"></df-info-icon>
        <nx-toolbar-divider></nx-toolbar-divider>
        <nx-switcher
          class="form-option"
          [disabled]="!isConfigValid"
          labelSize="small"
          labelPosition="left"
          [(ngModel)]="isRearrangeModeActive"
        >
          Rearrange
        </nx-switcher>
        <nx-switcher
          class="form-option"
          [disabled]="!isConfigValid"
          labelSize="small"
          labelPosition="left"
          [(ngModel)]="isAddFieldModeActive"
        >
          Add Formfields
        </nx-switcher>
        <div class="form-layout">
          <button
            nxPlainButton
            type="button"
            class="nx-margin-right-m"
            [disabled]="!isConfigValid"
            [nxPopoverTriggerFor]="popoverLayout"
            nxPopoverDirection="top"
            nxPopoverTrigger="click"
          >
            <nx-icon name="card-o" nxIconPositionEnd></nx-icon>
            Layout
          </button>
          <nx-popover #popoverLayout>
            <nx-formfield label="Layout">
              <nx-dropdown [value]="selectedLayoutType()">
                @for (option of layoutOptions; track option) {
                <nx-dropdown-item [value]="option.value" (click)="changeLayoutType(option.value)">
                  {{ option.label }}
                </nx-dropdown-item>
                }
              </nx-dropdown>
            </nx-formfield>
            <div class="popover-checkbox">
              <nx-checkbox
                class="nx-margin-bottom-s"
                (checkedChange)="onHeadlineWidthMatchesLayoutTypeChange($event)"
                [checked]="headlineWidthMatchesLayoutType()"
              >
                Headline width matches layout type
              </nx-checkbox>
              <df-info-icon
                class="nx-margin-left-xs"
                [config]="{
                  popoverDirection: 'bottom',
                  popoverText:
                    'When checked, it sets the *headlineWidthMatchesLayoutType* property to *true*.</br>This indicates whether headline fields should follow the layout columns or be displayed in full width. Ignored in CustomColumn.'
                }"
              ></df-info-icon>
            </div>
            <div class="popover-checkbox">
              <nx-checkbox
                class="nx-margin-bottom-s"
                (checkedChange)="onParagraphWidthMatchesLayoutTypeChange($event)"
                [checked]="paragraphWidthMatchesLayoutType()"
              >
                Paragraph width matches layout type
              </nx-checkbox>
              <df-info-icon
                class="nx-margin-left-xs"
                [config]="{
                  popoverDirection: 'bottom',
                  popoverText:
                    'When checked, it sets the *paragraphWidthMatchesLayoutType* property to *true*.</br>This indicates whether paragraph fields should follow the layout columns or be displayed in full width. Ignored in CustomColumn.'
                }"
              ></df-info-icon>
            </div>
            <div class="popover-checkbox">
              <nx-checkbox
                class="nx-margin-bottom-s"
                (checkedChange)="onFullWidthFormInExpertChange($event)"
                [checked]="fullWidthFormInExpert()"
              >
                Use full-width form in Expert mode
              </nx-checkbox>
              <df-info-icon
                class="nx-margin-left-xs"
                [config]="{
                  popoverDirection: 'bottom',
                  popoverText:
                    'When checked, it sets the *fullWidthFormInExpert* property to *true*.</br>This indicates whether the form should be displayed in full width in expert journeys.'
                }"
              ></df-info-icon>
            </div>
          </nx-popover>
        </div>
      </nx-toolbar>

      <nx-sidepanel-outer-container>
        <taly-dynamic-form-preview
          class="form-preview"
          id="dynamicForm"
          data-testid="dynamicForm"
          [formConfig]="formConfig()"
          [existingFormGroup]="formGroup"
          [highlightedPosition]="highlightedPosition()"
          [canEdit]="isRearrangeModeActive"
          [transformedValidationConfiguration]="transformedValidationConfiguration"
          (formfieldMoved)="moveFormfield($event.previousIndex, $event.currentIndex)"
          (formfieldAdded)="addFormfield($event.formfieldType, $event.index)"
        ></taly-dynamic-form-preview>

        <nx-sidepanel class="formfield-sidepanel" [opened]="isAddFieldModeActive" position="static">
          <nx-sidepanel-header class="formfield-sidepanel-header">
            <div>Formfields</div>
            <df-info-icon
              class="nx-margin-left-xs"
              [config]="{
                popoverDirection: 'bottom',
                popoverText:
                  'The formfields can be added to the form by either dragging and dropping them into the form preview on the left side or by clicking on them to add them to the end of the form.</br>You can learn more about the different formfield types [here](https://taly.frameworks.allianz.io/additional-documentation/dynamic-form.html#fields).'
              }"
            ></df-info-icon>
          </nx-sidepanel-header>

          <nx-sidepanel-content
            id="available-formfields"
            class="formfield-sidepanel-content"
            cdkDropList
            cdkDropListSortingDisabled=""
            [cdkDropListConnectedTo]="['editable-form']"
          >
            @for (formfield of formfieldTypes; track formfield ) {
            <button
              cdkDrag
              [cdkDragData]="{ identifier: formfield }"
              class="nx-margin-bottom-0"
              nxButton="tertiary"
              size="small"
              type="button"
              (click)="addFormfield(formfield)"
            >
              {{ formfield }}
              <div
                class="formfield-placeholder"
                *cdkDragPlaceholder
                [style.--grid-column-span]="placeholderGridColumns()"
              ></div>
            </button>
            }
          </nx-sidepanel-content>
        </nx-sidepanel>
      </nx-sidepanel-outer-container>
    </div>

    <div class="editor" right>
      <nx-toolbar>
        <nx-tab-group
          [(selectedIndex)]="editorTabIndex"
          [appearance]="'default'"
          (selectedIndexChange)="onTabIndexChange($event)"
        >
          <nx-tab label="Configuration"></nx-tab>
          <nx-tab label="Diff" [disabled]="!isConfigValid"></nx-tab>
          <nx-tab label="State" [disabled]="!isConfigValid"></nx-tab>
        </nx-tab-group>
        @if (!isConfigValid) {
        <nx-status-icon class="editor-errors" type="error" size="s"></nx-status-icon>
        }
      </nx-toolbar>
      @if (editorTabIndex === 0) { @if (appRootElementRef.shadowRoot) {
      <slot name="dfMonacoEditor"></slot>
      }
      <taly-dynamic-form-monaco-editor
        #dfMonacoEditorContainer
        slot="dfMonacoEditor"
        [showDiffEditor]="false"
        [formConfig]="formConfig()"
        (formConfigChange)="setNewConfigFromEditor($event)"
        (hasFormConfigErrors)="onFormConfigErrors($event)"
        (highlightedPosition)="highlightedPosition.set($event)"
      ></taly-dynamic-form-monaco-editor>
      } @else if (editorTabIndex === 1) { @if (appRootElementRef.shadowRoot) {
      <slot name="dfMonacoDiffEditor"></slot>
      }
      <taly-dynamic-form-monaco-editor
        #dfMonacoDiffEditorContainer
        slot="dfMonacoDiffEditor"
        [showDiffEditor]="true"
        [formConfig]="formConfig()"
        [originalFormConfig]="initialFormConfig"
      ></taly-dynamic-form-monaco-editor>
      } @else if (editorTabIndex === 2) {
      <ngx-json-viewer class="state-viewer" [json]="formGroup.value"></ngx-json-viewer>
      }
    </div>
  </taly-resizable-panels>
</div>

<div nxModalActions>
  @if (isServerAvailable()) {
  <button
    class="nx-margin-bottom-0"
    nxButton
    size="small"
    type="button"
    [disabled]="!hasChanges() || !isConfigValid"
    (click)="saveChanges()"
  >
    Save
  </button>
  <button
    class="nx-margin-bottom-0 nx-margin-left-xs"
    nxButton="secondary"
    size="small"
    type="button"
    (click)="discardChanges()"
  >
    Discard
  </button>
  } @else {
  <button class="nx-margin-bottom-0" nxButton size="small" type="button" (click)="closeModal()">
    Close
  </button>
  }
</div>

<ng-template #confirmationTemplate>
  <div>
    <h3 nxHeadline="subsection-medium" class="nx-modal-margin-bottom">
      Are you sure you want to discard your changes?
    </h3>
    <div>
      <button
        class="nx-margin-bottom-0 nx-margin-right-xs"
        nxButton
        size="small"
        type="button"
        critical
        (click)="confirmationDialogDiscardChanges()"
      >
        Discard
      </button>
      <button
        class="nx-margin-bottom-0"
        nxButton="secondary"
        size="small"
        type="button"
        (click)="cancelConfirmationDialog()"
      >
        Cancel
      </button>
    </div>
  </div>
</ng-template>

<ng-template #saveResultTemplate>
  <div>
    <h3 nxHeadline="subsection-medium" class="nx-modal-margin-bottom save-result-heading">
      @if (saveResultIsError()) {
      <nx-status-icon type="error" size="m"></nx-status-icon>
      } @else {
      <nx-status-icon type="success" size="m"></nx-status-icon>
      }
      {{ saveResultMessage() }}
    </h3>
    @if (saveResultIsError()) {
    <button
      class="nx-margin-bottom-0"
      nxButton
      size="small"
      type="button"
      (click)="closeSaveResultDialog()"
    >
      OK
    </button>
    }
  </div>
</ng-template>

./dynamic-form-editor.component.scss

:host {
  display: block;
  height: 100%;
}

.form-editor {
  height: calc(100% - 1px);
  margin: calc(-1 * var(--modal-padding));
  padding: 0 !important;

  .form {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 2;

    &-option {
      margin-right: 16px;
    }

    &-preview {
      flex: 1;
      padding: 48px 64px;
    }

    &-layout {
      margin-left: auto;
      > * {
        margin: 0 !important;
      }
    }

    .formfield-sidepanel {
      overflow: visible;

      &-header {
        padding: 16px;
        display: flex;
        justify-content: center;

        df-info-icon {
          display: flex;
        }
      }
      &-content {
        display: flex;
        flex-direction: column;
        overflow: visible;

        [cdkdrag] {
          z-index: 1;
        }
      }
    }
  }

  taly-dynamic-form-monaco-editor {
    height: 100%;
  }

  .editor {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 2;
    margin-left: 6px;
    overflow: hidden;

    > * {
      min-width: 0;
    }

    &-errors {
      margin-left: auto;
    }

    .state-viewer {
      margin: 8px;
    }
  }
}

:host:not(.is-a1) .form-editor {
  margin-bottom: 0;
}

.form-layout-toolbar {
  overflow-x: auto;
  overflow-y: hidden;
}

.formfield-placeholder {
  min-height: 64px;
  grid-column: span var(--grid-column-span, 1);
}

.popover-checkbox {
  display: flex;
}

nx-toolbar {
  min-height: 48px;
}

.save-result-heading {
  display: flex;
  align-items: center;
  gap: 16px;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""