Files
OpenNoodl/dev-docs/reference/REUSING-CODE-EDITORS.md
2026-01-08 13:27:38 +01:00

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 code
  • options.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 string
  • setValue(code: string): Set code
  • model: Access underlying Monaco model
  • dispose(): 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

  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 <CodeEditor model={model} ... />
  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