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

Implements

OnDestroy

Metadata

Relationships

Depends on

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor()

Inputs

formConfig
Type : DynamicFormConfiguration
Required :  true
originalFormConfig
Type : DynamicFormConfiguration
showDiffEditor
Type : boolean

Outputs

formConfigChange
Type : DynamicFormConfiguration
hasFormConfigErrors
Type : boolean
highlightedPosition
Type : number | undefined

Methods

onInitMonaco
onInitMonaco(editor: editor.IStandaloneCodeEditor)
Parameters :
Name Type Optional
editor editor.IStandaloneCodeEditor No
Returns : void

Properties

diffEditorOptions
Type : object
Default value : { theme: 'vs-light', language: 'json', readOnly: true, automaticLayout: false, scrollBeyondLastLine: false }
editorInitialized
Type : unknown
Default value : false
editorOptions
Type : object
Default value : { theme: 'vs-light', language: 'json', automaticLayout: false, scrollBeyondLastLine: false }
formControl
Type : unknown
Default value : new FormControl<string>('')
modifiedModel
Type : DiffEditorModel
originalModel
Type : DiffEditorModel
import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  DestroyRef,
  effect,
  ElementRef,
  inject,
  input,
  OnDestroy,
  output
} from '@angular/core';
import {
  DiffEditorModel,
  MonacoEditorModule,
  NGX_MONACO_EDITOR_CONFIG
} from '@allianz/taly-core/monaco-editor';
import { editor, IDisposable, Uri } from 'monaco-editor';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import type * as monaco from 'monaco-editor';
import { debounceTime } from 'rxjs';
import { distinctUntilChanged, filter } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { dynamicFormConfigSchema, DynamicFormConfiguration } from '@allianz/taly-core/schemas';
import { NxStatusIconComponent } from '@allianz/ng-aquila/icon';
import { NxHeadlineComponent } from '@allianz/ng-aquila/headline';
import { NxLinkComponent } from '@allianz/ng-aquila/link';

declare global {
  interface Window {
    monaco: typeof monaco;
  }
}

@Component({
  selector: 'taly-dynamic-form-monaco-editor',
  templateUrl: './dynamic-form-monaco-editor.component.html',
  styleUrls: ['./dynamic-form-monaco-editor.component.scss'],
  imports: [
    MonacoEditorModule,
    ReactiveFormsModule,
    NxStatusIconComponent,
    NxHeadlineComponent,
    NxLinkComponent
  ],
  providers: [
    {
      provide: NGX_MONACO_EDITOR_CONFIG,
      useValue: {
        baseUrl: window.location.origin + '/assets/monaco/min/vs'
      }
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DynamicFormMonacoEditorComponent implements OnDestroy {
  formConfig = input.required<DynamicFormConfiguration>();
  originalFormConfig = input<DynamicFormConfiguration>();
  showDiffEditor = input<boolean>();
  formConfigChange = output<DynamicFormConfiguration>();
  hasFormConfigErrors = output<boolean>();
  highlightedPosition = output<number | undefined>();
  editorOptions = {
    theme: 'vs-light',
    language: 'json',
    automaticLayout: false,
    scrollBeyondLastLine: false
  };
  diffEditorOptions = {
    theme: 'vs-light',
    language: 'json',
    readOnly: true,
    automaticLayout: false,
    scrollBeyondLastLine: false
  };
  originalModel!: DiffEditorModel;
  modifiedModel!: DiffEditorModel;
  formControl = new FormControl<string>('');
  editorInitialized = false;

  private editor?: editor.IStandaloneCodeEditor;
  private editorUri?: Uri;
  private listeners: IDisposable[] = [];
  private elementRef = inject(ElementRef);
  private resizeObserver?: ResizeObserver;
  private destroyRef = inject(DestroyRef);
  private cdr = inject(ChangeDetectorRef);

  constructor() {
    this.handleFormConfigControlChanges();
    effect(() => {
      this.updateFormConfigEditorContent(this.formConfig());
      this.setDiffConfigurations();
    });
  }

  ngOnDestroy(): void {
    this.editor?.dispose();
    this.resizeObserver?.disconnect();
    this.listeners.forEach((listener) => {
      listener.dispose();
    });
  }

  onInitMonaco(editor: editor.IStandaloneCodeEditor) {
    this.editor = editor;
    this.setFormSchema();
    this.validityConfiguration();
    this.configureFormfieldHighlight();
    this.resizeObserver = new ResizeObserver(() => editor.layout());
    this.resizeObserver.observe(this.elementRef.nativeElement);
    this.editorInitialized = true;
    this.cdr.detectChanges();
  }

  private setDiffConfigurations() {
    const originalConfigValue = JSON.stringify(this.originalFormConfig(), null, 2);
    const modifiedConfigValue = JSON.stringify(this.formConfig(), null, 2);
    this.originalModel = { code: originalConfigValue, language: 'json' };
    this.modifiedModel = { code: modifiedConfigValue, language: 'json' };
  }

  private handleFormConfigControlChanges() {
    this.formControl.valueChanges
      .pipe(
        debounceTime(500),
        distinctUntilChanged(),
        filter((data): data is NonNullable<typeof data> => data !== undefined),
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe((data) => {
        try {
          const parsedData = JSON.parse(data);
          this.formConfigChange.emit(parsedData);
        } catch {
          // Invalid JSON, do nothing
        }
      });
  }

  private updateFormConfigEditorContent(formConfig: DynamicFormConfiguration) {
    const configValue = JSON.stringify(formConfig, null, 2);
    const currentConfig = JSON.stringify(JSON.parse(this.formControl.value || '{}'), null, 2);
    if (configValue !== currentConfig) {
      this.formControl.setValue(configValue);
    }
  }

  private setFormSchema() {
    this.editorUri = this.editor?.getModel()?.uri;
    if (!this.editorUri) return;
    const existingSchemas = window.monaco.json.jsonDefaults.diagnosticsOptions?.schemas ?? [];
    window.monaco.json.jsonDefaults.setDiagnosticsOptions({
      validate: true,
      schemas: [
        ...existingSchemas,
        {
          fileMatch: [this.editorUri.toString()],
          uri: 'dynamic-form-config-schema',
          schema: dynamicFormConfigSchema
        }
      ]
    });
  }

  private validityConfiguration() {
    if (this.editorUri) {
      this.hasFormConfigErrors.emit(this.modelHasErrors(this.editorUri));
    }
    const markersListener = window.monaco?.editor.onDidChangeMarkers((uris) => {
      if (this.editorUri && uris.some((uri) => uri.toString() === this.editorUri?.toString())) {
        this.hasFormConfigErrors.emit(this.modelHasErrors(this.editorUri));
        this.cdr.markForCheck();
      }
    });

    if (markersListener) {
      this.listeners.push(markersListener);
    }
  }

  private modelHasErrors(uri: Uri): boolean {
    const markers = window.monaco?.editor.getModelMarkers({ resource: uri }) ?? [];
    return markers.length > 0;
  }

  private configureFormfieldHighlight() {
    if (this.editor && !this.showDiffEditor()) {
      // On focus lost
      this.listeners.push(
        this.editor?.onDidBlurEditorText(() => {
          this.highlightedPosition.emit(undefined);
          this.cdr.markForCheck();
        })
      );

      // On cursor position change
      this.listeners.push(
        this.editor?.onDidChangeCursorPosition((cursor) => {
          const position = cursor.position;
          this.highlightedPosition.emit(this.getFocusedFormfieldIndex(position));
          this.cdr.markForCheck();
        })
      );
    }
  }

  private getFocusedFormfieldIndex(position: monaco.Position): number | undefined {
    if (!this.editor) return undefined;

    const model = this.editor.getModel();
    if (!model) return undefined;

    try {
      const content = model.getValue();
      const offset = model.getOffsetAt(position);
      const parsed = JSON.parse(content) as DynamicFormConfiguration;
      const fields = parsed.fields || [];

      if (fields.length === 0) return undefined;

      // Find the fields array in the string
      const fieldsMatch = content.match(/"fields"\s*:\s*\[/);
      if (!fieldsMatch || !fieldsMatch.index) return undefined;

      const fieldsStartIndex = fieldsMatch.index + fieldsMatch[0].length;
      if (offset < fieldsStartIndex) return undefined;

      // Count opening braces to determine which field we're in
      let braceCount = 0;
      let fieldIndex = -1;
      let inString = false;
      let escapeNext = false;

      for (let i = fieldsStartIndex; i < offset && i < content.length; i++) {
        const char = content[i];

        if (escapeNext) {
          escapeNext = false;
          continue;
        }

        if (char === '\\') {
          escapeNext = true;
          continue;
        }

        if (char === '"') {
          inString = !inString;
          continue;
        }

        if (inString) continue;

        if (char === '{') {
          if (braceCount === 0) {
            fieldIndex++;
          }
          braceCount++;
        } else if (char === '}') {
          braceCount--;
        }
      }

      return fieldIndex >= 0 && fieldIndex < fields.length ? fieldIndex : undefined;
    } catch {
      return undefined;
    }
  }
}
@if (!editorInitialized) {
<div class="no-editor-message">
  <h1 class="no-editor-message-title" nxHeadline="subsection-small">
    <nx-status-icon class="nx-margin-right-xs" type="warning" size="s"></nx-status-icon>
    The editor is not available
  </h1>
  <div>Please make sure that the Monaco assets are imported correctly.</div>
  <div>
    Check out the
    <nx-link nxStyle="text"
      ><a
        href="https://taly.frameworks.allianz.io/additional-documentation/dynamic-form.html#dynamic-form-editor"
        target="”_blank”"
        >documentation</a
      ></nx-link
    >
    to learn more.
  </div>
</div>
} @if (!showDiffEditor()) {
<taly-monaco-editor
  class="editor monaco-editor"
  [options]="editorOptions"
  (onInit)="onInitMonaco($event)"
  [formControl]="formControl"
></taly-monaco-editor>
} @else {
<taly-monaco-diff-editor
  class="editor monaco-diff-editor"
  [options]="diffEditorOptions"
  [originalModel]="originalModel"
  [modifiedModel]="modifiedModel"
  (onInit)="onInitMonaco($event)"
></taly-monaco-diff-editor>
}

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

:host {
  display: block;

  .no-editor-message {
    display: flex;
    flex-direction: column;
    text-align: center;
    height: 100%;
    justify-content: center;

    &-title {
      display: flex;
      justify-content: center;
      align-items: center;
    }
  }
}

.editor {
  height: 100%;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""