11 KiB
Reusing Code Editors in OpenNoodl
This guide explains how to integrate Monaco code editors (the same editor as VS Code) into custom UI components in OpenNoodl.
Overview
OpenNoodl uses Monaco Editor for all code editing needs:
- JavaScript/TypeScript in Function and Script nodes
- JSON in Static Array node
- Plain text for other data types
The editor system is already set up and ready to reuse. You just need to know the pattern!
Core Components
1. Monaco Editor
The actual editor engine from VS Code.
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
2. EditorModel
Wraps a Monaco model with OpenNoodl-specific features (TypeScript support, etc.).
import { createModel } from '@noodl-utils/CodeEditor';
import { EditorModel } from '@noodl-utils/CodeEditor/model/editorModel';
3. CodeEditor Component
React component that renders the Monaco editor with toolbar and resizing.
import { CodeEditor, CodeEditorProps } from '@noodl-editor/views/panels/propertyeditor/CodeEditor/CodeEditor';
4. PopupLayer
Utility for showing popups (used for code editor popups).
import PopupLayer from '@noodl-editor/views/popuplayer';
Supported Languages
The createModel utility supports these languages:
| Language | Usage | Features |
|---|---|---|
javascript |
Function nodes | TypeScript checking, autocomplete, Noodl API types |
typescript |
Script nodes | Full TypeScript support |
json |
Static Array, Objects | JSON validation, formatting |
plaintext |
Other data | Basic text editing |
Basic Pattern (Inline Editor)
If you want an inline code editor (not in a popup):
import React, { useState } from 'react';
import { createModel } from '@noodl-utils/CodeEditor';
import { CodeEditor } from '../path/to/CodeEditor';
function MyComponent() {
// 1. Create the editor model
const model = createModel({
value: '[]', // Initial code
codeeditor: 'json' // Language
});
// 2. Render the editor
return (
<CodeEditor
model={model}
nodeId="my-unique-id" // For view state caching
onSave={() => {
const code = model.getValue();
console.log('Saved:', code);
}}
/>
);
}
Popup Pattern (Property Panel Style)
This is how the Function and Static Array nodes work - clicking a button opens a popup with the editor.
import React from 'react';
import { createRoot } from 'react-dom/client';
import { createModel } from '@noodl-utils/CodeEditor';
import { CodeEditor, CodeEditorProps } from '../path/to/CodeEditor';
import PopupLayer from '../path/to/popuplayer';
function openCodeEditorPopup(initialValue: string, onSave: (value: string) => void) {
// 1. Create model
const model = createModel({
value: initialValue,
codeeditor: 'json'
});
// 2. Create popup container
const popupDiv = document.createElement('div');
const root = createRoot(popupDiv);
// 3. Configure editor props
const props: CodeEditorProps = {
nodeId: 'my-editor-instance',
model: model,
initialSize: { x: 700, y: 500 },
onSave: () => {
const code = model.getValue();
onSave(code);
}
};
// 4. Render editor
root.render(React.createElement(CodeEditor, props));
// 5. Show popup
const button = document.querySelector('#my-button');
PopupLayer.showPopout({
content: { el: [popupDiv] },
attachTo: $(button),
position: 'right',
disableDynamicPositioning: true,
onClose: () => {
// Save and cleanup
const code = model.getValue();
onSave(code);
model.dispose();
root.unmount();
}
});
}
// Usage
<button
onClick={() =>
openCodeEditorPopup('[]', (code) => {
console.log('Saved:', code);
})
}
>
Edit JSON
</button>;
Full Example: JSON Editor for Array/Object Variables
Here's a complete example of integrating a JSON editor into a form:
import { CodeEditor, CodeEditorProps } from '@noodl-editor/views/panels/propertyeditor/CodeEditor/CodeEditor';
import PopupLayer from '@noodl-editor/views/popuplayer';
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { createModel } from '@noodl-utils/CodeEditor';
interface JSONEditorButtonProps {
value: string;
onChange: (value: string) => void;
type: 'array' | 'object';
}
function JSONEditorButton({ value, onChange, type }: JSONEditorButtonProps) {
const handleClick = () => {
// Create model
const model = createModel({
value: value,
codeeditor: 'json'
});
// Create popup
const popupDiv = document.createElement('div');
const root = createRoot(popupDiv);
const props: CodeEditorProps = {
nodeId: `json-editor-${type}`,
model: model,
initialSize: { x: 600, y: 400 },
onSave: () => {
try {
const code = model.getValue();
// Validate JSON
JSON.parse(code);
onChange(code);
} catch (e) {
console.error('Invalid JSON:', e);
}
}
};
root.render(React.createElement(CodeEditor, props));
PopupLayer.showPopout({
content: { el: [popupDiv] },
attachTo: $(event.currentTarget),
position: 'right',
onClose: () => {
props.onSave();
model.dispose();
root.unmount();
}
});
};
return <button onClick={handleClick}>Edit {type === 'array' ? 'Array' : 'Object'} ➜</button>;
}
// Usage
function MyForm() {
const [arrayValue, setArrayValue] = useState('[]');
return (
<div>
<label>My Array:</label>
<JSONEditorButton value={arrayValue} onChange={setArrayValue} type="array" />
</div>
);
}
Key APIs
createModel(options, node?)
Creates an EditorModel with Monaco model configured for a language.
Parameters:
options.value(string): Initial codeoptions.codeeditor(string): Language ID ('javascript','typescript','json','plaintext')node(optional): NodeGraphNode for TypeScript features
Returns: EditorModel
Example:
const model = createModel({
value: '{"key": "value"}',
codeeditor: 'json'
});
EditorModel Methods
getValue(): Get current code as stringsetValue(code: string): Set codemodel: Access underlying Monaco modeldispose(): Clean up (important!)
CodeEditor Props
interface CodeEditorProps {
nodeId: string; // Unique ID for view state caching
model: EditorModel; // The editor model
initialSize?: IVector2; // { x: width, y: height }
onSave: () => void; // Save callback
outEditor?: (editor) => void; // Get editor instance
}
Common Patterns
Pattern 1: Simple JSON Editor
For editing JSON data inline:
const model = createModel({ value: '{}', codeeditor: 'json' });
<CodeEditor
model={model}
nodeId="my-json"
onSave={() => {
const json = JSON.parse(model.getValue());
// Use json
}}
/>;
Pattern 2: JavaScript with TypeScript Checking
For scripts with type checking:
const model = createModel(
{
value: 'function myFunc() { }',
codeeditor: 'javascript'
},
nodeInstance
); // Pass node for types
Pattern 3: Popup on Button Click
For property panel-style editors:
<button
onClick={() => {
const model = createModel({ value, codeeditor: 'json' });
// Create popup (see full example above)
}}
>
Edit Code
</button>
Pitfalls & Solutions
❌ Pitfall: CRITICAL - Never Bypass createModel()
This is the #1 mistake that causes worker errors!
// ❌ WRONG - Bypasses worker configuration
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
const model = monaco.editor.createModel(value, 'json');
// Result: "Error: Unexpected usage" worker errors!
// ✅ CORRECT - Use createModel utility
import { createModel } from '@noodl-utils/CodeEditor';
const model = createModel({
type: 'array', // or 'object', 'string'
value: value,
codeeditor: 'javascript' // arrays/objects use this!
});
// Result: Works perfectly, no worker errors
Why this matters:
createModel()configures TypeScript/JavaScript workers properly- Direct Monaco API skips this configuration
- You get "Cannot use import statement outside a module" errors
- Always use
createModel()- it's already set up for you!
❌ Pitfall: Forgetting to dispose
// BAD - Memory leak
const model = createModel({...});
// Never disposed!
// GOOD - Always dispose
const model = createModel({...});
// ... use model ...
model.dispose(); // Clean up when done
❌ Pitfall: Invalid JSON crashes
// BAD - No validation
const code = model.getValue();
const json = JSON.parse(code); // Throws if invalid!
// GOOD - Validate first
try {
const code = model.getValue();
const json = JSON.parse(code);
// Use json
} catch (e) {
console.error('Invalid JSON');
}
❌ Pitfall: Using wrong language
// BAD - Language doesn't match data
createModel({ value: '{"json": true}', codeeditor: 'javascript' });
// No JSON validation!
// GOOD - Match language to data type
createModel({ value: '{"json": true}', codeeditor: 'json' });
// Proper validation
Testing Your Integration
- Open the editor - Does it appear correctly?
- Syntax highlighting - Is JSON/JS highlighted?
- Error detection - Enter invalid JSON, see red squiggles?
- Auto-format - Press Ctrl+Shift+F, does it format?
- Save works - Edit and save, does
onSavetrigger? - Resize works - Can you drag to resize?
- Close works - Does it cleanup on close?
Where It's Used in OpenNoodl
Study these for real examples:
| Location | What | Language |
|---|---|---|
packages/noodl-viewer-react/src/nodes/std-library/data/staticdata.js |
Static Array node | JSON |
packages/noodl-editor/src/editor/src/views/panels/propertyeditor/CodeEditor/CodeEditorType.ts |
Property panel integration | All |
packages/noodl-editor/src/editor/src/views/panels/propertyeditor/components/AiChat/AiChat.tsx |
AI code editor | JavaScript |
Summary
To reuse code editors:
- Import
createModelandCodeEditor - Create a model with
createModel({ value, codeeditor }) - Render
<CodeEditor model={model} ... /> - Handle
onSavecallback - Dispose model when done
For popups (recommended):
- Use
PopupLayer.showPopout() - Render editor into popup div
- Clean up in
onClose
Last Updated: January 2025