src/app/features/reporting/edit-sql-query/sql-code-editor.component.ts
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).
CustomFormControlDirective<string>
EditComponent
AfterViewInit
OnDestroy
| changeDetection | ChangeDetectionStrategy.OnPush |
| encapsulation | ViewEncapsulation.None |
| providers |
SqlCodeEditorComponent
|
| selector | app-edit-sql-query |
| standalone | true |
| styleUrls | ./sql-code-editor.component.scss |
| template | |
| styleUrl | ./sql-code-editor.component.scss |
Properties |
|
Methods |
Inputs |
Outputs |
constructor()
|
| formFieldConfig | |
Type : FormFieldConfig
|
|
| aria-describedby | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
| disabled | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
| ngControl | |
Type : any
|
|
Default value : inject(NgControl, { optional: true, self: true })
|
|
|
Inherited from
CustomFormControlDirective
|
|
| placeholder | |
Type : string
|
|
|
Inherited from
CustomFormControlDirective
|
|
| required | |
Type : boolean
|
|
|
Inherited from
CustomFormControlDirective
|
|
| value | |
Type : T
|
|
|
Inherited from
CustomFormControlDirective
|
|
| valueChange | |
Type : EventEmitter
|
|
|
Inherited from
CustomFormControlDirective
|
|
| blur |
blur()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| focus |
focus()
|
|
Inherited from
CustomFormControlDirective
|
|
Returns :
void
|
| onContainerClick | ||||||
onContainerClick(event: MouseEvent)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| setDescribedByIds | ||||||
setDescribedByIds(ids: string[])
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
|
Inherited from
CustomFormControlDirective
|
||||||
|
Parameters :
Returns :
void
|
| writeValue | |||||||||||||||
writeValue(value: T, notifyFormControl: unknown)
|
|||||||||||||||
|
Inherited from
CustomFormControlDirective
|
|||||||||||||||
|
Implementation for Angular ControlValueAccessor interface that links the form control value to the component value
Parameters :
Returns :
void
|
| controlType |
Type : string
|
Default value : "custom-control"
|
|
Inherited from
CustomFormControlDirective
|
| elementRef |
Type : unknown
|
Default value : inject<ElementRef<HTMLElement>>(ElementRef)
|
|
Inherited from
CustomFormControlDirective
|
| Readonly enabled |
Type : Signal<boolean>
|
Default value : computed(() => !this._disabled())
|
|
Inherited from
CustomFormControlDirective
|
|
Whether the control is currently enabled, as a signal (tracks disabled). |
| errorStateMatcher |
Type : unknown
|
Default value : inject(ErrorStateMatcher)
|
|
Inherited from
CustomFormControlDirective
|
| id |
Type : unknown
|
Default value : `custom-form-control-${CustomFormControlDirective.nextId++}`
|
|
Inherited from
CustomFormControlDirective
|
| Static nextId |
Type : number
|
Default value : 0
|
|
Inherited from
CustomFormControlDirective
|
| onChange |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
| onTouched |
Type : unknown
|
Default value : () => {...}
|
|
Inherited from
CustomFormControlDirective
|
| parentForm |
Type : unknown
|
Default value : inject(NgForm, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
| parentFormGroup |
Type : unknown
|
Default value : inject(FormGroupDirective, { optional: true })
|
|
Inherited from
CustomFormControlDirective
|
| stateChanges |
Type : unknown
|
Default value : new Subject<void>()
|
|
Inherited from
CustomFormControlDirective
|
| Readonly valueSignal |
Type : Signal<T>
|
Default value : computed(() => this._value())
|
|
Inherited from
CustomFormControlDirective
|
|
The current value of the control as a signal.
Authoritative in both modes: it reflects the bound |
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;
}
}