mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 23:32:55 +01:00
Finished Blockly prototype, updated project template json
This commit is contained in:
618
dev-docs/reference/LEARNINGS-BLOCKLY.md
Normal file
618
dev-docs/reference/LEARNINGS-BLOCKLY.md
Normal file
@@ -0,0 +1,618 @@
|
||||
# Blockly Integration Learnings
|
||||
|
||||
**Created:** 2026-01-12
|
||||
**Source:** TASK-012 Blockly Logic Builder Integration
|
||||
**Context:** Building a visual programming interface with Google Blockly in OpenNoodl
|
||||
|
||||
## Overview
|
||||
|
||||
This document captures critical learnings from integrating Google Blockly into OpenNoodl to create the Logic Builder node. These patterns are essential for anyone working with Blockly or integrating visual programming tools into the editor.
|
||||
|
||||
## Critical Architecture Patterns
|
||||
|
||||
### 1. Editor/Runtime Window Separation 🔴 CRITICAL
|
||||
|
||||
**The Problem:**
|
||||
|
||||
The OpenNoodl editor and runtime run in COMPLETELY SEPARATE JavaScript contexts (different windows/iframes). This is easy to forget and causes mysterious bugs.
|
||||
|
||||
**What Breaks:**
|
||||
|
||||
```javascript
|
||||
// ❌ BROKEN - In runtime, trying to access editor objects
|
||||
function updatePorts(nodeId, workspace, editorConnection) {
|
||||
// This looks reasonable but FAILS silently
|
||||
const graphModel = getGraphModel(); // Doesn't exist in runtime!
|
||||
const node = graphModel.getNodeWithId(nodeId); // Crashes here
|
||||
const code = node.parameters.generatedCode;
|
||||
}
|
||||
```
|
||||
|
||||
**The Fix:**
|
||||
|
||||
```javascript
|
||||
// ✅ WORKING - Pass data explicitly as parameters
|
||||
function updatePorts(nodeId, workspace, generatedCode, editorConnection) {
|
||||
// generatedCode passed directly - no cross-window access needed
|
||||
const detected = parseCode(generatedCode);
|
||||
editorConnection.sendDynamicPorts(nodeId, detected.ports);
|
||||
}
|
||||
|
||||
// In editor: Pass the data explicitly
|
||||
updatePorts(node.id, node.parameters.workspace, node.parameters.generatedCode, connection);
|
||||
```
|
||||
|
||||
**Key Principle:**
|
||||
|
||||
> **NEVER** assume editor objects/methods are available in runtime. **ALWAYS** pass data explicitly through function parameters or event payloads.
|
||||
|
||||
**Applies To:**
|
||||
|
||||
- Any dynamic port detection
|
||||
- Code generation systems
|
||||
- Parameter passing between editor and runtime
|
||||
- Event payloads between windows
|
||||
|
||||
---
|
||||
|
||||
### 2. Function Execution Context 🔴 CRITICAL
|
||||
|
||||
**The Problem:**
|
||||
|
||||
Using `new Function(code).call(context)` doesn't work as expected. The generated code can't access variables via `this`.
|
||||
|
||||
**What Breaks:**
|
||||
|
||||
```javascript
|
||||
// ❌ BROKEN - Generated code can't access Outputs
|
||||
const fn = new Function(code); // Code contains: Outputs["result"] = 'test';
|
||||
fn.call(context); // context has Outputs property
|
||||
|
||||
// Result: ReferenceError: Outputs is not defined
|
||||
```
|
||||
|
||||
**The Fix:**
|
||||
|
||||
```javascript
|
||||
// ✅ WORKING - Pass context as function parameters
|
||||
const fn = new Function(
|
||||
'Inputs', // Parameter names
|
||||
'Outputs',
|
||||
'Noodl',
|
||||
'Variables',
|
||||
'Objects',
|
||||
'Arrays',
|
||||
'sendSignalOnOutput',
|
||||
code // Function body
|
||||
);
|
||||
|
||||
// Call with actual values as arguments
|
||||
fn(
|
||||
context.Inputs,
|
||||
context.Outputs,
|
||||
context.Noodl,
|
||||
context.Variables,
|
||||
context.Objects,
|
||||
context.Arrays,
|
||||
context.sendSignalOnOutput
|
||||
);
|
||||
```
|
||||
|
||||
**Why This Works:**
|
||||
|
||||
The function parameters create a proper lexical scope where the generated code can access variables by name.
|
||||
|
||||
**Code Generator Pattern:**
|
||||
|
||||
```javascript
|
||||
// When generating code, reference parameters directly
|
||||
javascriptGenerator.forBlock['noodl_set_output'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const value = javascriptGenerator.valueToCode(block, 'VALUE', Order.ASSIGNMENT);
|
||||
|
||||
// Generated code uses parameter name directly
|
||||
return `Outputs["${name}"] = ${value};\n`;
|
||||
};
|
||||
```
|
||||
|
||||
**Key Principle:**
|
||||
|
||||
> **ALWAYS** pass execution context as function parameters. **NEVER** rely on `this` or `.call()` for context in dynamically compiled code.
|
||||
|
||||
---
|
||||
|
||||
### 3. Blockly v10+ API Compatibility 🟡 IMPORTANT
|
||||
|
||||
**The Problem:**
|
||||
|
||||
Blockly v10+ uses a completely different API from older versions. Documentation and examples online are often outdated.
|
||||
|
||||
**What Breaks:**
|
||||
|
||||
```javascript
|
||||
// ❌ BROKEN - Old API (pre-v10)
|
||||
import * as Blockly from 'blockly';
|
||||
|
||||
import 'blockly/javascript';
|
||||
|
||||
// These don't exist in v10+:
|
||||
Blockly.JavaScript.ORDER_MEMBER;
|
||||
Blockly.JavaScript.ORDER_ASSIGNMENT;
|
||||
Blockly.JavaScript.workspaceToCode(workspace);
|
||||
```
|
||||
|
||||
**The Fix:**
|
||||
|
||||
```javascript
|
||||
// ✅ WORKING - Modern v10+ API
|
||||
import * as Blockly from 'blockly';
|
||||
import { javascriptGenerator, Order } from 'blockly/javascript';
|
||||
|
||||
// Use named exports
|
||||
Order.MEMBER;
|
||||
Order.ASSIGNMENT;
|
||||
javascriptGenerator.workspaceToCode(workspace);
|
||||
```
|
||||
|
||||
**Complete Migration Guide:**
|
||||
|
||||
| Old API (pre-v10) | New API (v10+) |
|
||||
| -------------------------------------- | -------------------------------------------- |
|
||||
| `Blockly.JavaScript.ORDER_*` | `Order.*` from `blockly/javascript` |
|
||||
| `Blockly.JavaScript['block_type']` | `javascriptGenerator.forBlock['block_type']` |
|
||||
| `Blockly.JavaScript.workspaceToCode()` | `javascriptGenerator.workspaceToCode()` |
|
||||
| `Blockly.JavaScript.valueToCode()` | `javascriptGenerator.valueToCode()` |
|
||||
|
||||
**Key Principle:**
|
||||
|
||||
> **ALWAYS** use named imports from `blockly/javascript`. Check Blockly version first before following online examples.
|
||||
|
||||
---
|
||||
|
||||
### 4. Z-Index Layering (React + Legacy Canvas) 🟡 IMPORTANT
|
||||
|
||||
**The Problem:**
|
||||
|
||||
React overlays on legacy jQuery/canvas systems can be invisible if z-index isn't explicitly set.
|
||||
|
||||
**What Breaks:**
|
||||
|
||||
```html
|
||||
<!-- ❌ BROKEN - Tabs invisible behind canvas -->
|
||||
<div id="canvas-tabs-root" style="width: 100%; height: 100%">
|
||||
<div class="tabs">...</div>
|
||||
</div>
|
||||
<canvas id="canvas" style="position: absolute; top: 0; left: 0">
|
||||
<!-- Canvas renders ON TOP of tabs! -->
|
||||
</canvas>
|
||||
```
|
||||
|
||||
**The Fix:**
|
||||
|
||||
```html
|
||||
<!-- ✅ WORKING - Explicit z-index layering -->
|
||||
<div id="canvas-tabs-root" style="position: absolute; z-index: 100; pointer-events: none">
|
||||
<div class="tabs" style="pointer-events: all">
|
||||
<!-- Tabs visible and clickable -->
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="canvas" style="position: absolute; top: 0; left: 0">
|
||||
<!-- Canvas in background -->
|
||||
</canvas>
|
||||
```
|
||||
|
||||
**Pointer Events Strategy:**
|
||||
|
||||
1. **Container:** `pointer-events: none` (transparent to clicks)
|
||||
2. **Content:** `pointer-events: all` (captures clicks)
|
||||
3. **Result:** Canvas clickable when no tabs, tabs clickable when present
|
||||
|
||||
**CSS Pattern:**
|
||||
|
||||
```scss
|
||||
#canvas-tabs-root {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100; // Above canvas
|
||||
pointer-events: none; // Transparent when empty
|
||||
}
|
||||
|
||||
.CanvasTabs {
|
||||
pointer-events: all; // Clickable when rendered
|
||||
}
|
||||
```
|
||||
|
||||
**Key Principle:**
|
||||
|
||||
> In mixed legacy/React systems, **ALWAYS** set explicit `position` and `z-index`. Use `pointer-events` to manage click-through behavior.
|
||||
|
||||
---
|
||||
|
||||
## Blockly-Specific Patterns
|
||||
|
||||
### Block Registration
|
||||
|
||||
**Must Call Before Workspace Creation:**
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - Blocks never registered
|
||||
useEffect(() => {
|
||||
const workspace = Blockly.inject(...); // Fails - blocks don't exist yet
|
||||
}, []);
|
||||
|
||||
// ✅ CORRECT - Register first, then inject
|
||||
useEffect(() => {
|
||||
initBlocklyIntegration(); // Registers custom blocks
|
||||
const workspace = Blockly.inject(...); // Now blocks exist
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Initialization Guard Pattern:**
|
||||
|
||||
```typescript
|
||||
let blocklyInitialized = false;
|
||||
|
||||
export function initBlocklyIntegration() {
|
||||
if (blocklyInitialized) return; // Safe to call multiple times
|
||||
|
||||
// Register blocks
|
||||
Blockly.Blocks['my_block'] = {...};
|
||||
javascriptGenerator.forBlock['my_block'] = function(block) {...};
|
||||
|
||||
blocklyInitialized = true;
|
||||
}
|
||||
```
|
||||
|
||||
### Toolbox Configuration
|
||||
|
||||
**Categories Must Reference Registered Blocks:**
|
||||
|
||||
```typescript
|
||||
function getDefaultToolbox() {
|
||||
return {
|
||||
kind: 'categoryToolbox',
|
||||
contents: [
|
||||
{
|
||||
kind: 'category',
|
||||
name: 'My Blocks',
|
||||
colour: 230,
|
||||
contents: [
|
||||
{ kind: 'block', type: 'my_block' } // Must match Blockly.Blocks key
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Workspace Persistence
|
||||
|
||||
**Save/Load Pattern:**
|
||||
|
||||
```typescript
|
||||
// Save to JSON
|
||||
const json = Blockly.serialization.workspaces.save(workspace);
|
||||
const workspaceStr = JSON.stringify(json);
|
||||
onSave(workspaceStr);
|
||||
|
||||
// Load from JSON
|
||||
const json = JSON.parse(workspaceStr);
|
||||
Blockly.serialization.workspaces.load(json, workspace);
|
||||
```
|
||||
|
||||
### Code Generation Pattern
|
||||
|
||||
**Block Definition:**
|
||||
|
||||
```javascript
|
||||
Blockly.Blocks['noodl_set_output'] = {
|
||||
init: function () {
|
||||
this.appendValueInput('VALUE')
|
||||
.setCheck(null)
|
||||
.appendField('set output')
|
||||
.appendField(new Blockly.FieldTextInput('result'), 'NAME');
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
this.setColour(230);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Code Generator:**
|
||||
|
||||
```javascript
|
||||
javascriptGenerator.forBlock['noodl_set_output'] = function (block, generator) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const value = generator.valueToCode(block, 'VALUE', Order.ASSIGNMENT) || '""';
|
||||
|
||||
// Return JavaScript code
|
||||
return `Outputs["${name}"] = ${value};\n`;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Port Detection
|
||||
|
||||
### Regex Parsing (MVP Pattern)
|
||||
|
||||
For MVP, simple regex parsing is sufficient:
|
||||
|
||||
```javascript
|
||||
function detectOutputPorts(generatedCode) {
|
||||
const outputs = [];
|
||||
const regex = /Outputs\["([^"]+)"\]/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(generatedCode)) !== null) {
|
||||
const name = match[1];
|
||||
if (!outputs.find((o) => o.name === name)) {
|
||||
outputs.push({ name, type: '*' });
|
||||
}
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
```
|
||||
|
||||
**When To Use:**
|
||||
|
||||
- MVP/prototypes
|
||||
- Simple output detection
|
||||
- Known code patterns
|
||||
|
||||
**When To Upgrade:**
|
||||
|
||||
- Need input detection
|
||||
- Signal detection
|
||||
- Complex expressions
|
||||
- AST-based analysis needed
|
||||
|
||||
### AST Parsing (Future Pattern)
|
||||
|
||||
For production, use proper AST parsing:
|
||||
|
||||
```javascript
|
||||
import * as acorn from 'acorn';
|
||||
|
||||
function detectPorts(code) {
|
||||
const ast = acorn.parse(code, { ecmaVersion: 2020 });
|
||||
const detected = { inputs: [], outputs: [], signals: [] };
|
||||
|
||||
// Walk AST and detect patterns
|
||||
walk(ast, {
|
||||
MemberExpression(node) {
|
||||
if (node.object.name === 'Outputs') {
|
||||
detected.outputs.push(node.property.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return detected;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Coordination Patterns
|
||||
|
||||
### Editor → Runtime Communication
|
||||
|
||||
**Use Event Payloads:**
|
||||
|
||||
```javascript
|
||||
// Editor side
|
||||
EventDispatcher.instance.notifyListeners('LogicBuilder.Updated', {
|
||||
nodeId: node.id,
|
||||
workspace: workspaceJSON,
|
||||
generatedCode: code // Send all needed data
|
||||
});
|
||||
|
||||
// Runtime side
|
||||
graphModel.on('parameterUpdated', function (event) {
|
||||
if (event.name === 'generatedCode') {
|
||||
const code = node.parameters.generatedCode; // Now available
|
||||
updatePorts(node.id, workspace, code, editorConnection);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Canvas Visibility Coordination
|
||||
|
||||
**EventDispatcher Pattern:**
|
||||
|
||||
```javascript
|
||||
// When Logic Builder tab opens
|
||||
EventDispatcher.instance.notifyListeners('LogicBuilder.TabOpened');
|
||||
|
||||
// Canvas hides itself
|
||||
EventDispatcher.instance.on('LogicBuilder.TabOpened', () => {
|
||||
setCanvasVisibility(false);
|
||||
});
|
||||
|
||||
// When all tabs closed
|
||||
EventDispatcher.instance.notifyListeners('LogicBuilder.AllTabsClosed');
|
||||
|
||||
// Canvas shows itself
|
||||
EventDispatcher.instance.on('LogicBuilder.AllTabsClosed', () => {
|
||||
setCanvasVisibility(true);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### ❌ Don't: Wrap Legacy in React
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - Trying to render canvas in React
|
||||
function CanvasTabs() {
|
||||
return (
|
||||
<div>
|
||||
<div id="canvas-container">{/* Can't put canvas here - it's rendered by vanilla JS */}</div>
|
||||
<LogicBuilderTab />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Do: Separate Concerns
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Canvas and React separate
|
||||
// Canvas always rendered by vanilla JS
|
||||
// React tabs overlay when needed
|
||||
|
||||
function CanvasTabs() {
|
||||
return tabs.length > 0 ? (
|
||||
<div className="overlay">
|
||||
{tabs.map((tab) => (
|
||||
<Tab key={tab.id} {...tab} />
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Don't: Assume Shared Context
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - Accessing editor from runtime
|
||||
function runtimeFunction() {
|
||||
const model = ProjectModel.instance; // Doesn't exist in runtime!
|
||||
const node = model.getNode(nodeId);
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Do: Pass Data Explicitly
|
||||
|
||||
```javascript
|
||||
// ✅ CORRECT - Data passed as parameters
|
||||
function runtimeFunction(nodeId, data, connection) {
|
||||
// All data provided explicitly
|
||||
processData(data);
|
||||
connection.sendResult(nodeId, result);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategies
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
- [ ] Blocks appear in toolbox
|
||||
- [ ] Blocks draggable onto workspace
|
||||
- [ ] Workspace saves correctly
|
||||
- [ ] Code generation works
|
||||
- [ ] Dynamic ports appear
|
||||
- [ ] Execution triggers
|
||||
- [ ] Output values flow
|
||||
- [ ] Tabs manageable (open/close)
|
||||
- [ ] Canvas switching works
|
||||
- [ ] Z-index layering correct
|
||||
|
||||
### Debug Logging Pattern
|
||||
|
||||
```javascript
|
||||
// Temporary debug logs (remove before production)
|
||||
console.log('[BlocklyWorkspace] Code generated:', code.substring(0, 100));
|
||||
console.log('[Logic Builder] Detected ports:', detectedPorts);
|
||||
console.log('[Runtime] Execution context:', Object.keys(context));
|
||||
```
|
||||
|
||||
**Remove or gate behind flag:**
|
||||
|
||||
```javascript
|
||||
const DEBUG = false; // Set via environment variable
|
||||
|
||||
if (DEBUG) {
|
||||
console.log('[Debug] Important info:', data);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Blockly Workspace Size
|
||||
|
||||
- Small projects (<50 blocks): No issues
|
||||
- Medium (50-200 blocks): Slight lag on load
|
||||
- Large (>200 blocks): Consider workspace pagination
|
||||
|
||||
### Code Generation
|
||||
|
||||
- Generated code is cached (only regenerates on change)
|
||||
- Regex parsing is O(n) where n = code length (fast enough)
|
||||
- AST parsing is slower but more accurate
|
||||
|
||||
### React Re-renders
|
||||
|
||||
```typescript
|
||||
// Memoize expensive operations
|
||||
const toolbox = useMemo(() => getDefaultToolbox(), []);
|
||||
const workspace = useMemo(() => createWorkspace(toolbox), [toolbox]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Input Port Detection
|
||||
|
||||
```javascript
|
||||
// Detect: Inputs["myInput"]
|
||||
const inputRegex = /Inputs\["([^"]+)"\]/g;
|
||||
```
|
||||
|
||||
### Signal Output Detection
|
||||
|
||||
```javascript
|
||||
// Detect: sendSignalOnOutput("mySignal")
|
||||
const signalRegex = /sendSignalOnOutput\s*\(\s*["']([^"']+)["']\s*\)/g;
|
||||
```
|
||||
|
||||
### Block Marketplace
|
||||
|
||||
- User-contributed blocks
|
||||
- Import/export block definitions
|
||||
- Block versioning system
|
||||
|
||||
### Visual Debugging
|
||||
|
||||
- Step through blocks execution
|
||||
- Variable inspection
|
||||
- Breakpoints in visual logic
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
1. **Editor and runtime are SEPARATE windows** - never forget this
|
||||
2. **Pass context as function parameters** - not via `this`
|
||||
3. **Use Blockly v10+ API** - check imports carefully
|
||||
4. **Set explicit z-index** - don't rely on DOM order
|
||||
5. **Keep legacy and React separate** - coordinate via events
|
||||
6. **Initialize blocks before workspace** - order matters
|
||||
7. **Test with real user flow** - early and often
|
||||
8. **Document discoveries immediately** - while context is fresh
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Blockly Documentation](https://developers.google.com/blockly)
|
||||
- [OpenNoodl TASK-012 Complete](../tasks/phase-3-editor-ux-overhaul/TASK-012-blockly-integration/)
|
||||
- [Window Context Patterns](./LEARNINGS-RUNTIME.md#window-separation)
|
||||
- [Z-Index Layering](./LEARNINGS.md#react-legacy-integration)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-01-12
|
||||
**Maintainer:** Development Team
|
||||
**Status:** Production-Ready Patterns
|
||||
@@ -4,6 +4,482 @@ This document captures important discoveries and gotchas encountered during Open
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ CRITICAL ARCHITECTURE PATTERNS
|
||||
|
||||
These fundamental patterns apply across ALL Noodl development. Understanding them prevents hours of debugging.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Editor/Runtime Window Separation (Jan 2026)
|
||||
|
||||
### The Invisible Boundary: Why Editor Methods Don't Exist in Runtime
|
||||
|
||||
**Context**: TASK-012 Blockly Integration - Discovered that editor and runtime run in completely separate JavaScript contexts (different windows/iframes). This is THE most important architectural detail to understand.
|
||||
|
||||
**CRITICAL PRINCIPLE**: The OpenNoodl editor and runtime are NOT in the same JavaScript execution context. They are separate windows that communicate via message passing.
|
||||
|
||||
**What This Means**:
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ Editor Window │ Message │ Runtime Window │
|
||||
│ │ Passing │ │
|
||||
│ - ProjectModel │←-------→│ - Node execution │
|
||||
│ - NodeGraphEditor │ │ - Dynamic ports │
|
||||
│ - graphModel │ │ - Code compilation │
|
||||
│ - UI components │ │ - No editor access! │
|
||||
└─────────────────────┘ └─────────────────────┘
|
||||
```
|
||||
|
||||
**The Broken Pattern**:
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - In runtime node code, trying to access editor
|
||||
function updatePorts(nodeId, workspace, editorConnection) {
|
||||
// These look reasonable but FAIL silently or crash:
|
||||
const graphModel = getGraphModel(); // ☠️ Doesn't exist in runtime!
|
||||
const node = graphModel.getNodeWithId(nodeId); // ☠️ graphModel is undefined
|
||||
const code = node.parameters.generatedCode; // ☠️ Can't access node this way
|
||||
|
||||
// Problem: Runtime has NO ACCESS to editor objects/methods
|
||||
}
|
||||
```
|
||||
|
||||
**The Correct Pattern**:
|
||||
|
||||
```javascript
|
||||
// ✅ RIGHT - Pass ALL data explicitly via parameters
|
||||
function updatePorts(nodeId, workspace, generatedCode, editorConnection) {
|
||||
// generatedCode passed directly - no cross-window access needed
|
||||
const detected = parseCode(generatedCode);
|
||||
editorConnection.sendDynamicPorts(nodeId, detected.ports);
|
||||
|
||||
// All data provided explicitly through function parameters
|
||||
}
|
||||
|
||||
// In editor: Pass the data explicitly when calling
|
||||
const node = graphModel.getNodeWithId(nodeId);
|
||||
updatePorts(
|
||||
node.id,
|
||||
node.parameters.workspace,
|
||||
node.parameters.generatedCode, // ✅ Pass explicitly
|
||||
editorConnection
|
||||
);
|
||||
```
|
||||
|
||||
**Why This Matters**:
|
||||
|
||||
- **Silent failures**: Attempting to access editor objects from runtime often fails silently
|
||||
- **Mysterious undefined errors**: "Cannot read property X of undefined" when objects don't exist
|
||||
- **Debugging nightmare**: Looks like your code is wrong when it's an architecture issue
|
||||
- **Affects ALL editor/runtime communication**: Dynamic ports, code generation, parameter updates
|
||||
|
||||
**Common Mistakes**:
|
||||
|
||||
1. Looking up nodes in graphModel from runtime
|
||||
2. Accessing ProjectModel from runtime
|
||||
3. Trying to call editor methods from node setup functions
|
||||
4. Assuming shared global scope between editor and runtime
|
||||
|
||||
**Critical Rules**:
|
||||
|
||||
1. **NEVER** assume editor objects exist in runtime code
|
||||
2. **ALWAYS** pass data explicitly through function parameters
|
||||
3. **NEVER** look up nodes via graphModel from runtime
|
||||
4. **ALWAYS** use event payloads with complete data
|
||||
5. **TREAT** editor and runtime as separate processes that only communicate via messages
|
||||
|
||||
**Applies To**:
|
||||
|
||||
- Dynamic port detection systems
|
||||
- Code generation and compilation
|
||||
- Parameter updates and node configuration
|
||||
- Custom property editors
|
||||
- Any feature bridging editor and runtime
|
||||
|
||||
**Detection**:
|
||||
|
||||
- Runtime errors about undefined objects that "should exist"
|
||||
- Functions that work in editor but fail in runtime
|
||||
- Dynamic features that don't update when they should
|
||||
- Silent failures with no error messages
|
||||
|
||||
**Time Saved**: Understanding this architectural boundary can save 2-4 hours PER feature that crosses the editor/runtime divide.
|
||||
|
||||
**Location**: Discovered in TASK-012 Blockly Integration (Logic Builder dynamic ports)
|
||||
|
||||
**Keywords**: editor runtime separation, window context, iframe, cross-context communication, graphModel, ProjectModel, dynamic ports, architecture boundary
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Dynamic Code Compilation Context (Jan 2026)
|
||||
|
||||
### The this Trap: Why new Function() + .call() Doesn't Work
|
||||
|
||||
**Context**: TASK-012 Blockly Integration - Generated code failed with "ReferenceError: Outputs is not defined" despite context being passed via `.call()`.
|
||||
|
||||
**CRITICAL PRINCIPLE**: When using `new Function()` to compile user code dynamically, execution context MUST be passed as function parameters, NOT via `this` or `.call()`.
|
||||
|
||||
**The Problem**: Modern JavaScript scoping rules make `this` unreliable for providing execution context to dynamically compiled code.
|
||||
|
||||
**The Broken Pattern**:
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - Generated code can't access context variables
|
||||
const fn = new Function(code); // Code contains: Outputs["result"] = 'test';
|
||||
fn.call(context); // context = { Outputs: {}, Inputs: {}, Noodl: {...} }
|
||||
|
||||
// Result: ReferenceError: Outputs is not defined
|
||||
// Why: Generated code has no lexical access to context properties
|
||||
```
|
||||
|
||||
**The Correct Pattern**:
|
||||
|
||||
```javascript
|
||||
// ✅ RIGHT - Pass context as function parameters
|
||||
const fn = new Function(
|
||||
'Inputs', // Parameter names define lexical scope
|
||||
'Outputs',
|
||||
'Noodl',
|
||||
'Variables',
|
||||
'Objects',
|
||||
'Arrays',
|
||||
'sendSignalOnOutput',
|
||||
code // Function body - can reference parameters by name
|
||||
);
|
||||
|
||||
// Call with actual values as arguments
|
||||
fn(
|
||||
context.Inputs,
|
||||
context.Outputs,
|
||||
context.Noodl,
|
||||
context.Variables,
|
||||
context.Objects,
|
||||
context.Arrays,
|
||||
context.sendSignalOnOutput
|
||||
);
|
||||
|
||||
// Generated code: Outputs["result"] = 'test'; // ✅ Works! Outputs is in scope
|
||||
```
|
||||
|
||||
**Why This Works**:
|
||||
|
||||
Function parameters create a proper lexical scope where the generated code can access variables by their parameter names. This is how closures and scope work in JavaScript.
|
||||
|
||||
**Code Generator Pattern**:
|
||||
|
||||
```javascript
|
||||
// When generating code, reference parameters directly
|
||||
javascriptGenerator.forBlock['set_output'] = function (block) {
|
||||
const name = block.getFieldValue('NAME');
|
||||
const value = javascriptGenerator.valueToCode(block, 'VALUE', Order.ASSIGNMENT);
|
||||
|
||||
// Generated code uses parameter name directly - no 'context.' prefix needed
|
||||
return `Outputs["${name}"] = ${value};\n`;
|
||||
};
|
||||
|
||||
// Result: Outputs["result"] = "hello"; // Parameter name, not property access
|
||||
```
|
||||
|
||||
**Comparison with eval()** (Don't use eval, but this explains the difference):
|
||||
|
||||
```javascript
|
||||
// eval() has access to surrounding scope (dangerous!)
|
||||
const context = { Outputs: {} };
|
||||
eval('Outputs["result"] = "test"'); // Works but unsafe
|
||||
|
||||
// new Function() creates isolated scope (safe!)
|
||||
const fn = new Function('Outputs', 'Outputs["result"] = "test"');
|
||||
fn(context.Outputs); // Safe and works
|
||||
```
|
||||
|
||||
**Critical Rules**:
|
||||
|
||||
1. **ALWAYS** pass execution context as function parameters
|
||||
2. **NEVER** rely on `this` or `.call()` for context in compiled code
|
||||
3. **GENERATE** code that references parameters directly, not properties
|
||||
4. **LIST** all context variables as function parameters
|
||||
5. **PASS** arguments in same order as parameters
|
||||
|
||||
**Applies To**:
|
||||
|
||||
- Expression node evaluation
|
||||
- JavaScript Function node execution
|
||||
- Logic Builder block code generation
|
||||
- Any dynamic code compilation system
|
||||
- Script evaluation in custom nodes
|
||||
|
||||
**Common Mistakes**:
|
||||
|
||||
1. Using `.call(context)` and expecting generated code to access context properties
|
||||
2. Using `.apply(context, args)` but not listing context as parameters
|
||||
3. Generating code with `context.Outputs` instead of just `Outputs`
|
||||
4. Forgetting to pass an argument for every parameter
|
||||
|
||||
**Detection**:
|
||||
|
||||
- "ReferenceError: [variable] is not defined" when executing compiled code
|
||||
- Variables exist in context but code can't access them
|
||||
- `.call()` or `.apply()` used but doesn't provide access
|
||||
- Generated code works in eval() but not new Function()
|
||||
|
||||
**Time Saved**: This pattern prevents 1-2 hours of debugging per dynamic code feature. The error message gives no clue that the problem is parameter passing.
|
||||
|
||||
**Location**: Discovered in TASK-012 Blockly Integration (Logic Builder execution)
|
||||
|
||||
**Keywords**: new Function, dynamic code, compilation, execution context, this, call, apply, parameters, lexical scope, ReferenceError, code generation
|
||||
|
||||
---
|
||||
|
||||
## 🎨 React Overlay Z-Index Pattern (Jan 2026)
|
||||
|
||||
### The Invisible UI: Why React Overlays Disappear Behind Canvas
|
||||
|
||||
**Context**: TASK-012 Blockly Integration - React tabs were invisible because canvas layers rendered on top. This is a universal problem when adding React to legacy canvas systems.
|
||||
|
||||
**CRITICAL PRINCIPLE**: When overlaying React components on legacy HTML5 Canvas or jQuery systems, DOM order alone is INSUFFICIENT. You MUST set explicit `position` and `z-index`.
|
||||
|
||||
**The Problem**: Absolute-positioned canvas layers render based on z-index, not DOM order. React overlays without explicit z-index appear behind the canvas.
|
||||
|
||||
**The Broken Pattern**:
|
||||
|
||||
```html
|
||||
<!-- ❌ WRONG - React overlay invisible behind canvas -->
|
||||
<div id="react-overlay-root" style="width: 100%; height: 100%">
|
||||
<div class="tabs">Tab controls here</div>
|
||||
</div>
|
||||
<canvas id="legacy-canvas" style="position: absolute; top: 0; left: 0">
|
||||
<!-- Canvas renders ON TOP even though it's after in DOM! -->
|
||||
</canvas>
|
||||
```
|
||||
|
||||
**The Correct Pattern**:
|
||||
|
||||
```html
|
||||
<!-- ✅ RIGHT - Explicit z-index layering -->
|
||||
<div id="react-overlay-root" style="position: absolute; z-index: 100; pointer-events: none">
|
||||
<div class="tabs" style="pointer-events: all">
|
||||
<!-- Tabs visible and clickable -->
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="legacy-canvas" style="position: absolute; top: 0; left: 0">
|
||||
<!-- Canvas in background (no z-index or z-index < 100) -->
|
||||
</canvas>
|
||||
```
|
||||
|
||||
**Pointer Events Strategy**: The click-through pattern
|
||||
|
||||
1. **Container**: `pointer-events: none` (transparent to clicks)
|
||||
2. **Content**: `pointer-events: all` (captures clicks)
|
||||
3. **Result**: Canvas clickable when no React UI, React UI clickable when present
|
||||
|
||||
**CSS Pattern**:
|
||||
|
||||
```scss
|
||||
#react-overlay-root {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100; // Above canvas
|
||||
pointer-events: none; // Transparent when empty
|
||||
}
|
||||
|
||||
.ReactUIComponent {
|
||||
pointer-events: all; // Clickable when rendered
|
||||
}
|
||||
```
|
||||
|
||||
**Layer Stack** (Bottom → Top):
|
||||
|
||||
```
|
||||
z-index: 100 ← React overlays (tabs, panels, etc.)
|
||||
z-index: 50 ← Canvas overlays (comments, highlights)
|
||||
z-index: 1 ← Main canvas
|
||||
z-index: 0 ← Background layers
|
||||
```
|
||||
|
||||
**Why This Matters**:
|
||||
|
||||
- **Silent failure**: UI renders but is invisible (no errors)
|
||||
- **Works in isolation**: React components work fine in Storybook
|
||||
- **Fails in integration**: Same components invisible when added to canvas
|
||||
- **Not obvious**: DevTools show elements exist but can't see them
|
||||
|
||||
**Critical Rules**:
|
||||
|
||||
1. **ALWAYS** set `position: absolute` or `fixed` on overlay containers
|
||||
2. **ALWAYS** set explicit `z-index` higher than canvas (e.g., 100)
|
||||
3. **ALWAYS** use `pointer-events: none` on containers
|
||||
4. **ALWAYS** use `pointer-events: all` on interactive content
|
||||
5. **NEVER** rely on DOM order for layering with absolute positioning
|
||||
|
||||
**Applies To**:
|
||||
|
||||
- Any React overlay on canvas (tabs, panels, dialogs)
|
||||
- Canvas visualization views
|
||||
- Debug overlays and dev tools
|
||||
- Custom editor tools and widgets
|
||||
- Future canvas integration features
|
||||
|
||||
**Common Mistakes**:
|
||||
|
||||
1. Forgetting `position: absolute` on overlay (it won't stack correctly)
|
||||
2. Not setting `z-index` (canvas wins by default)
|
||||
3. Not using `pointer-events` management (blocks canvas clicks)
|
||||
4. Setting z-index on wrong element (set on container, not children)
|
||||
|
||||
**Detection**:
|
||||
|
||||
- React component renders in React DevTools but not visible
|
||||
- Element exists in DOM inspector but can't see it
|
||||
- Clicking canvas area triggers React component (wrong z-order)
|
||||
- Works in Storybook but invisible in editor
|
||||
|
||||
**Time Saved**: This pattern prevents 1-3 hours of "why is my UI invisible" debugging per overlay feature.
|
||||
|
||||
**Location**: Discovered in TASK-012B Blockly Integration (Tab visibility fix)
|
||||
|
||||
**Keywords**: z-index, React overlay, canvas layering, position absolute, pointer-events, click-through, DOM order, stacking context, legacy integration
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Legacy/React Separation Pattern (Jan 2026)
|
||||
|
||||
### The Wrapper Trap: Why You Can't Render Canvas in React
|
||||
|
||||
**Context**: TASK-012 Blockly Integration - Initial attempt to wrap canvas in React tabs failed catastrophically. Canvas rendering broke completely.
|
||||
|
||||
**CRITICAL PRINCIPLE**: NEVER try to render legacy vanilla JS or jQuery code inside React components. Keep them completely separate and coordinate via events.
|
||||
|
||||
**The Problem**: Legacy canvas systems manage their own DOM, lifecycle, and rendering. React's virtual DOM and component lifecycle conflict with this, causing rendering failures, memory leaks, and crashes.
|
||||
|
||||
**The Broken Pattern**:
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - Trying to wrap canvas in React
|
||||
function EditorTabs() {
|
||||
return (
|
||||
<div>
|
||||
<TabBar />
|
||||
<div id="canvas-container">
|
||||
{/* Can't put vanilla JS canvas here! */}
|
||||
{/* Canvas is rendered by nodegrapheditor.ts, not React */}
|
||||
</div>
|
||||
<BlocklyTab />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Result: Canvas rendering breaks, tabs don't work, memory leaks
|
||||
```
|
||||
|
||||
**The Correct Pattern**: Separation of Concerns
|
||||
|
||||
```typescript
|
||||
// ✅ RIGHT - Canvas and React completely separate
|
||||
|
||||
// Canvas rendered by vanilla JS (always present)
|
||||
// In nodegrapheditor.ts:
|
||||
const canvas = document.getElementById('nodegraphcanvas');
|
||||
renderCanvas(canvas); // Legacy rendering
|
||||
|
||||
// React tabs overlay when needed (conditional)
|
||||
function EditorTabs() {
|
||||
return tabs.length > 0 ? (
|
||||
<div className="overlay">
|
||||
{tabs.map((tab) => (
|
||||
<Tab key={tab.id} {...tab} />
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
// Coordinate visibility via EventDispatcher
|
||||
EventDispatcher.instance.on('BlocklyTabOpened', () => {
|
||||
setCanvasVisibility(false); // Hide canvas
|
||||
});
|
||||
|
||||
EventDispatcher.instance.on('BlocklyTabClosed', () => {
|
||||
setCanvasVisibility(true); // Show canvas
|
||||
});
|
||||
```
|
||||
|
||||
**Architecture**: Desktop vs Windows Metaphor
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ React Tabs (Windows) │ ← Overlay when needed
|
||||
├────────────────────────────────────┤
|
||||
│ Canvas (Desktop) │ ← Always rendered
|
||||
└────────────────────────────────────┘
|
||||
|
||||
• Canvas = Desktop: Always there, rendered by vanilla JS
|
||||
• React Tabs = Windows: Appear/disappear, managed by React
|
||||
• Coordination = Events: Show/hide via EventDispatcher
|
||||
```
|
||||
|
||||
**Why This Matters**:
|
||||
|
||||
- **Canvas lifecycle independence**: Canvas manages its own rendering, events, state
|
||||
- **React lifecycle conflicts**: React wants to control DOM, canvas already controls it
|
||||
- **Memory leaks**: Re-rendering React components can duplicate canvas instances
|
||||
- **Event handler chaos**: Both systems try to manage the same DOM events
|
||||
|
||||
**Critical Rules**:
|
||||
|
||||
1. **NEVER** put legacy canvas/jQuery in React component JSX
|
||||
2. **ALWAYS** keep legacy always-rendered in background
|
||||
3. **ALWAYS** coordinate visibility via EventDispatcher, not React state
|
||||
4. **NEVER** try to control canvas lifecycle from React
|
||||
5. **TREAT** them as separate systems that coordinate, don't integrate
|
||||
|
||||
**Coordination Pattern**:
|
||||
|
||||
```typescript
|
||||
// React component listens to canvas events
|
||||
useEventListener(NodeGraphEditor.instance, 'viewportChanged', (viewport) => {
|
||||
// Update React state based on canvas events
|
||||
});
|
||||
|
||||
// Canvas listens to React events
|
||||
EventDispatcher.instance.on('ReactUIAction', (data) => {
|
||||
// Canvas responds to React UI changes
|
||||
});
|
||||
```
|
||||
|
||||
**Applies To**:
|
||||
|
||||
- Canvas integration (node graph editor)
|
||||
- Any legacy jQuery code in the editor
|
||||
- Third-party libraries with their own rendering
|
||||
- Future integrations with non-React systems
|
||||
- Plugin systems or external tools
|
||||
|
||||
**Common Mistakes**:
|
||||
|
||||
1. Trying to `ReactDOM.render(<Canvas />)` with legacy canvas
|
||||
2. Putting canvas container in React component tree
|
||||
3. Managing canvas visibility with React state instead of CSS
|
||||
4. Attempting to "React-ify" legacy code instead of coordinating
|
||||
|
||||
**Detection**:
|
||||
|
||||
- Canvas stops rendering after React component mounts
|
||||
- Multiple canvas instances created (memory leak)
|
||||
- Event handlers fire multiple times
|
||||
- Canvas rendering flickers or behaves erratically
|
||||
- DOM manipulation conflicts (React vs vanilla JS)
|
||||
|
||||
**Time Saved**: Understanding this pattern saves 4-8 hours of debugging per integration attempt. Prevents architectural dead-ends.
|
||||
|
||||
**Location**: Discovered in TASK-012B Blockly Integration (Canvas visibility coordination)
|
||||
|
||||
**Keywords**: React legacy integration, canvas React, vanilla JS React, jQuery React, separation of concerns, EventDispatcher coordination, lifecycle management, DOM conflicts
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Node Color Scheme Must Match Defined Colors (Jan 11, 2026)
|
||||
|
||||
### The Undefined Colors Crash: When Node Picker Can't Find Color Scheme
|
||||
|
||||
Reference in New Issue
Block a user