# 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. ```typescript import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; ``` ### 2. EditorModel Wraps a Monaco model with OpenNoodl-specific features (TypeScript support, etc.). ```typescript 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. ```typescript import { CodeEditor, CodeEditorProps } from '@noodl-editor/views/panels/propertyeditor/CodeEditor/CodeEditor'; ``` ### 4. PopupLayer Utility for showing popups (used for code editor popups). ```typescript 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): ```tsx 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 ( { 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. ```tsx 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 ; ``` --- ## Full Example: JSON Editor for Array/Object Variables Here's a complete example of integrating a JSON editor into a form: ```tsx 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 ; } // Usage function MyForm() { const [arrayValue, setArrayValue] = useState('[]'); return (
); } ``` --- ## Key APIs ### createModel(options, node?) Creates an EditorModel with Monaco model configured for a language. **Parameters:** - `options.value` (string): Initial code - `options.codeeditor` (string): Language ID (`'javascript'`, `'typescript'`, `'json'`, `'plaintext'`) - `node` (optional): NodeGraphNode for TypeScript features **Returns:** `EditorModel` **Example:** ```typescript const model = createModel({ value: '{"key": "value"}', codeeditor: 'json' }); ``` ### EditorModel Methods - `getValue()`: Get current code as string - `setValue(code: string)`: Set code - `model`: Access underlying Monaco model - `dispose()`: Clean up (important!) ### CodeEditor Props ```typescript 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: ```typescript const model = createModel({ value: '{}', codeeditor: 'json' }); { const json = JSON.parse(model.getValue()); // Use json }} />; ``` ### Pattern 2: JavaScript with TypeScript Checking For scripts with type checking: ```typescript const model = createModel( { value: 'function myFunc() { }', codeeditor: 'javascript' }, nodeInstance ); // Pass node for types ``` ### Pattern 3: Popup on Button Click For property panel-style editors: ```typescript ``` --- ## Pitfalls & Solutions ### ❌ Pitfall: CRITICAL - Never Bypass createModel() **This is the #1 mistake that causes worker errors!** ```typescript // ❌ 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! ``` ```typescript // ✅ 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 ```typescript // BAD - Memory leak const model = createModel({...}); // Never disposed! ``` ```typescript // GOOD - Always dispose const model = createModel({...}); // ... use model ... model.dispose(); // Clean up when done ``` ### ❌ Pitfall: Invalid JSON crashes ```typescript // BAD - No validation const code = model.getValue(); const json = JSON.parse(code); // Throws if invalid! ``` ```typescript // 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 ```typescript // BAD - Language doesn't match data createModel({ value: '{"json": true}', codeeditor: 'javascript' }); // No JSON validation! ``` ```typescript // GOOD - Match language to data type createModel({ value: '{"json": true}', codeeditor: 'json' }); // Proper validation ``` --- ## Testing Your Integration 1. **Open the editor** - Does it appear correctly? 2. **Syntax highlighting** - Is JSON/JS highlighted? 3. **Error detection** - Enter invalid JSON, see red squiggles? 4. **Auto-format** - Press Ctrl+Shift+F, does it format? 5. **Save works** - Edit and save, does `onSave` trigger? 6. **Resize works** - Can you drag to resize? 7. **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:** 1. Import `createModel` and `CodeEditor` 2. Create a model with `createModel({ value, codeeditor })` 3. Render `` 4. Handle `onSave` callback 5. Dispose model when done **For popups** (recommended): - Use `PopupLayer.showPopout()` - Render editor into popup div - Clean up in `onClose` --- _Last Updated: January 2025_