libs/core/devtools/dynamic-form-editor/src/lib/dynamic-form-editor/dynamic-form-editor.component.ts
OnInit
OnDestroy
AfterViewInit
| changeDetection | ChangeDetectionStrategy.OnPush |
| host | { |
| providers |
provideDynamicFormNativeComponents()
|
| selector | taly-dynamic-form-editor |
| standalone | true |
| imports |
NxToolbarComponent
NxToolbarDividerComponent
NxSwitcherComponent
NxPlainButtonComponent
NxPopoverTriggerDirective
NxPopoverComponent
NxFormfieldComponent
NxDropdownComponent
NxDropdownItemComponent
NxSidepanelOuterContainerComponent
NxButtonComponent
NxTabGroupComponent
NxTabComponent
NxHeadlineComponent
NxModalActionsDirective
NxModalContentDirective
NxSidepanelComponent
NxIconComponent
NxStatusIconComponent
NxCheckboxComponent
|
| styleUrls | ./dynamic-form-editor.component.scss |
| templateUrl | ./dynamic-form-editor.component.html |
No results matching.
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
provideDynamicFormNativeComponents()
OnInit
OnDestroy
AfterViewInit
constructor()
|
| addFormfield |
addFormfield(formfieldType: string, index?: number)
|
|
Returns :
void
|
| cancelConfirmationDialog |
cancelConfirmationDialog()
|
|
Returns :
void
|
| changeLayoutType | ||||||
changeLayoutType(layout: DfFormLayoutType)
|
||||||
|
Parameters :
Returns :
void
|
| closeModal |
closeModal()
|
|
Returns :
void
|
| closeSaveResultDialog |
closeSaveResultDialog()
|
|
Returns :
void
|
| confirmationDialogDiscardChanges |
confirmationDialogDiscardChanges()
|
|
Returns :
void
|
| discardChanges |
discardChanges()
|
|
Returns :
void
|
| moveFormfield |
moveFormfield(previousIndex: number, currentIndex: number)
|
|
Returns :
void
|
| onFormConfigErrors | ||||||
onFormConfigErrors(hasErrors: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| onFullWidthFormInExpertChange | ||||||
onFullWidthFormInExpertChange(checked: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| onHeadlineWidthMatchesLayoutTypeChange | ||||||
onHeadlineWidthMatchesLayoutTypeChange(checked: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| onParagraphWidthMatchesLayoutTypeChange | ||||||
onParagraphWidthMatchesLayoutTypeChange(checked: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| onTabIndexChange | ||||||
onTabIndexChange(index: number)
|
||||||
|
Parameters :
Returns :
void
|
| saveChanges |
saveChanges()
|
|
Returns :
void
|
| setNewConfigFromEditor | ||||||
setNewConfigFromEditor(newConfig: DynamicFormConfiguration)
|
||||||
|
Parameters :
Returns :
void
|
| 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
)
|
| 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;
}