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>
}
.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 with directive