src/app/features/reporting/edit-sql-query/sql-code-editor.component.ts

Description

Edit a single SQL query string with CodeMirror 6 syntax highlighting.

Works both as a form-field editComponent (bound via ngControl) and standalone via [value] / (valueChange) (used by EditReportDefinitionComponent for each query).

Extends

CustomFormControlDirective<string>

Implements

EditComponent AfterViewInit OnDestroy

Example

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor()

Inputs

formFieldConfig
Type : FormFieldConfig
aria-describedby
Type : string
disabled
Type : boolean
ngControl
Type : any
Default value : inject(NgControl, { optional: true, self: true })
placeholder
Type : string
required
Type : boolean
value
Type : T

Outputs

valueChange
Type : EventEmitter

Methods

blur
blur()
Returns : void
focus
focus()
Returns : void
onContainerClick
onContainerClick(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
setDescribedByIds
setDescribedByIds(ids: string[])
Parameters :
Name Type Optional
ids string[] No
Returns : void
setDisabledState
setDisabledState(isDisabled: boolean)
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void
writeValue
writeValue(value: T, notifyFormControl: unknown)

Implementation for Angular ControlValueAccessor interface that links the form control value to the component value

Parameters :
Name Type Optional Default value Description
value T No

The new value to set

notifyFormControl unknown No false

Whether to notify the FormControl of this change (for internal updates)

Returns : void

Properties

controlType
Type : string
Default value : "custom-control"
elementRef
Type : unknown
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
Readonly enabled
Type : Signal<boolean>
Default value : computed(() => !this._disabled())

Whether the control is currently enabled, as a signal (tracks disabled).

errorStateMatcher
Type : unknown
Default value : inject(ErrorStateMatcher)
id
Type : unknown
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
Static nextId
Type : number
Default value : 0
onChange
Type : unknown
Default value : () => {...}
onTouched
Type : unknown
Default value : () => {...}
parentForm
Type : unknown
Default value : inject(NgForm, { optional: true })
parentFormGroup
Type : unknown
Default value : inject(FormGroupDirective, { optional: true })
stateChanges
Type : unknown
Default value : new Subject<void>()
Readonly valueSignal
Type : Signal<T>
Default value : computed(() => this._value())

The current value of the control as a signal. Authoritative in both modes: it reflects the bound FormControl (synced in ngDoCheck) as well as [(value)] / writeValue updates.

import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  effect,
  ElementRef,
  input,
  OnDestroy,
  viewChild,
  ViewEncapsulation,
} from "@angular/core";
import { MatFormFieldControl } from "@angular/material/form-field";
import { Compartment, EditorState } from "@codemirror/state";
import { EditorView, keymap, lineNumbers } from "@codemirror/view";
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import {
  bracketMatching,
  defaultHighlightStyle,
  syntaxHighlighting,
} from "@codemirror/language";
import { sql } from "@codemirror/lang-sql";
import { CustomFormControlDirective } from "#src/app/core/common-components/basic-autocomplete/custom-form-control.directive";
import { FormFieldConfig } from "#src/app/core/common-components/entity-form/FormConfig";
import { DynamicComponent } from "#src/app/core/config/dynamic-components/dynamic-component.decorator";
import { EditComponent } from "#src/app/core/entity/entity-field-edit/dynamic-edit/edit-component.interface";

/**
 * Edit a single SQL query string with CodeMirror 6 syntax highlighting.
 *
 * Works both as a form-field `editComponent` (bound via `ngControl`) and standalone via
 * `[value]` / `(valueChange)` (used by {@link EditReportDefinitionComponent} for each query).
 */
@DynamicComponent("EditSqlQuery")
@Component({
  selector: "app-edit-sql-query",
  template: `<div #editor class="sql-code-editor"></div>`,
  styleUrl: "./sql-code-editor.component.scss",
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  providers: [
    { provide: MatFormFieldControl, useExisting: SqlCodeEditorComponent },
  ],
})
export class SqlCodeEditorComponent
  extends CustomFormControlDirective<string>
  implements EditComponent, AfterViewInit, OnDestroy
{
  formFieldConfig = input<FormFieldConfig>();

  private readonly editorHost =
    viewChild.required<ElementRef<HTMLDivElement>>("editor");
  private editor?: EditorView;
  private readonly editable = new Compartment();
  /** guards the update listener from echoing programmatic (external) doc updates */
  private applyingExternalValue = false;

  constructor() {
    super();

    // push external value changes (form load/reset, [value] binding) into the editor
    effect(() => {
      const value = this.valueSignal() ?? "";
      const editor = this.editor;
      if (!editor) {
        return;
      }
      const current = editor.state.doc.toString();
      if (value !== current) {
        this.applyingExternalValue = true;
        editor.dispatch({
          changes: { from: 0, to: current.length, insert: value },
        });
        this.applyingExternalValue = false;
      }
    });

    // reflect the enabled/disabled state in the editor
    effect(() => {
      const enabled = this.enabled();
      this.editor?.dispatch({
        effects: this.editable.reconfigure(EditorView.editable.of(enabled)),
      });
    });
  }

  ngAfterViewInit(): void {
    const state = EditorState.create({
      doc: this.valueSignal() ?? "",
      extensions: [
        lineNumbers(),
        history(),
        bracketMatching(),
        syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
        sql(),
        keymap.of([...defaultKeymap, ...historyKeymap]),
        EditorView.lineWrapping,
        this.editable.of(EditorView.editable.of(this.enabled())),
        EditorView.updateListener.of((update) => {
          if (update.docChanged && !this.applyingExternalValue) {
            this.value = update.state.doc.toString();
          }
        }),
      ],
    });

    this.editor = new EditorView({
      state,
      parent: this.editorHost().nativeElement,
    });
  }

  override ngOnDestroy(): void {
    this.editor?.destroy();
    this.editor = undefined;
    super.ngOnDestroy();
  }
}

./sql-code-editor.component.scss

// ViewEncapsulation.None: scope all rules under the host class so they don't leak.
// CodeMirror builds its DOM (`.cm-editor`) imperatively, so it is not reachable by
// Angular's emulated encapsulation and must be styled globally (under this class).
.sql-code-editor {
  .cm-editor {
    border: 1px solid rgba(0, 0, 0, 0.12);
    border-radius: 4px;
    min-height: 5em;
    font-size: 13px;
    background: #fff;
  }

  .cm-editor.cm-focused {
    outline: none;
    border-color: rgba(0, 0, 0, 0.38);
  }

  .cm-scroller {
    overflow: auto;
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""