Refactor: Make JsonEditorComponent Reactive with Standardized JSON Formatting & Safe Synchronization
Problem Statement
Currently, the JsonEditorComponent serves as a wrapper around Monaco Editor, but it leaks its internal Monaco ITextModel APIs to its parent consumers. Whenever a parent component needs to populate or format the editor content, it is forced to reach deep into the editor's internals to fetch the model and manually format the value:
// Pattern repeated in multiple runner components
const model = this.inputEditor()?.editor?.getModel();
if (model) {
try {
model.setValue(JSON.stringify(JSON.parse(content), null, 4));
} catch {
model.setValue(content);
}
}
This leads to several architectural issues:
- Encapsulation Leaks: Parents must directly manipulate Monaco's private model, making future editor upgrades or replacements fragile.
- Boilerplate Duplication: There are at least 6 distinct places (including
AppContextSidebar, ActionRunner, FlowRunner, PromptRunner, RetrieverRunner, and DatasetDetailsPage) doing identical try-catch / JSON.stringify manual formatting.
- Imperative Programming: Parent components must hook into child-loaded events (
(editorLoaded)) to push data imperatively, rather than binding state declaratively.
Proposed Solution
We should refactor JsonEditorComponent to support a reactive, declarative API by exposing a value input signal. To do this safely—without resetting the user's active text cursor or discarding Monaco's undo/redo history during typing—we can implement a Safe Synchronization pattern with a structural equality guard.
1. Update JsonEditorComponent API
Expose a single, unified value input signal and a internal sync effect inside json-editor.component.ts:
export class JsonEditorComponent {
// Declarative input for any raw string, object, or nullish value
value = input<string | object | null | undefined>();
constructor() {
// Automatically synchronize state changes
effect(() => {
this.syncEditorValue(this.value());
});
}
private syncEditorValue(newValue: string | object | null | undefined) {
if (!this.editor) return;
const model = this.editor.getModel();
if (!model) return;
const formattedNewValue = this.formatValue(newValue);
const currentValue = model.getValue();
// Prevent resetting Monaco cursor/undo stack if value hasn't structurally changed
if (this.areJsonValuesEqual(currentValue, formattedNewValue)) {
return;
}
model.setValue(formattedNewValue);
}
private formatValue(
val: string | object | null | undefined,
indent = 4
): string {
if (val === null || val === undefined) return '';
if (typeof val === 'object') return JSON.stringify(val, null, indent);
try {
const parsed = JSON.parse(val);
return JSON.stringify(parsed, null, indent);
} catch {
return val; // Keep user's active keystrokes intact even if JSON is temporarily invalid
}
}
private areJsonValuesEqual(a: string, b: string): boolean {
if (a === b) return true;
try {
return JSON.stringify(JSON.parse(a)) === JSON.stringify(JSON.parse(b));
} catch {
return false;
}
}
}
Impact & Refactored Code Samples
Implementing this reduces the complexity of all parent components down to a single declarative property binding in their templates:
Example: AppContextSidebar
Before:
Component TS:
onEditorLoaded() {
const editor = this.contextEditor();
if (editor) {
const model = editor.editor?.getModel();
if (model) {
model.setValue(this.contextService.contextString());
}
}
}
Component HTML:
<json-editor
#contextEditor
(editorLoaded)="onEditorLoaded()"
(contentChangeEvent)="onValueChange($event)" />
After:
Component TS:
(All onEditorLoaded boilerplate is eliminated)
Component HTML:
<json-editor
[value]="contextService.contextString()"
(contentChangeEvent)="onValueChange($event)" />
Benefits
- Strict Encapsulation: Monaco model APIs remain entirely private to
JsonEditorComponent.
- DRY Codebase: Eliminates manual formatting boilerplate and error-handling from 6+ parent components.
- Improved UX: Automatic and consistent pretty-printing on load, with built-in cursor and history protection as the user edits.
- Declarative Patterns: Aligns with modern Angular standards by replacing imperative hooks with clean signal-based data flows.
Refactor: Make
JsonEditorComponentReactive with Standardized JSON Formatting & Safe SynchronizationProblem Statement
Currently, the
JsonEditorComponentserves as a wrapper around Monaco Editor, but it leaks its internal MonacoITextModelAPIs to its parent consumers. Whenever a parent component needs to populate or format the editor content, it is forced to reach deep into the editor's internals to fetch the model and manually format the value:This leads to several architectural issues:
AppContextSidebar,ActionRunner,FlowRunner,PromptRunner,RetrieverRunner, andDatasetDetailsPage) doing identicaltry-catch/JSON.stringifymanual formatting.(editorLoaded)) to push data imperatively, rather than binding state declaratively.Proposed Solution
We should refactor
JsonEditorComponentto support a reactive, declarative API by exposing avalueinput signal. To do this safely—without resetting the user's active text cursor or discarding Monaco's undo/redo history during typing—we can implement a Safe Synchronization pattern with a structural equality guard.1. Update
JsonEditorComponentAPIExpose a single, unified
valueinput signal and a internal sync effect insidejson-editor.component.ts:Impact & Refactored Code Samples
Implementing this reduces the complexity of all parent components down to a single declarative property binding in their templates:
Example:
AppContextSidebarBefore:
Component TS:
Component HTML:
After:
Component TS:
(All
onEditorLoadedboilerplate is eliminated)Component HTML:
Benefits
JsonEditorComponent.