15 KiB
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:
// ❌ 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:
// ✅ 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:
// ❌ 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:
// ✅ 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:
// 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
thisor.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:
// ❌ 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:
// ✅ 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:
<!-- ❌ 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:
<!-- ✅ 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:
- Container:
pointer-events: none(transparent to clicks) - Content:
pointer-events: all(captures clicks) - Result: Canvas clickable when no tabs, tabs clickable when present
CSS Pattern:
#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
positionandz-index. Usepointer-eventsto manage click-through behavior.
Blockly-Specific Patterns
Block Registration
Must Call Before Workspace Creation:
// ❌ 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:
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:
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:
// 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:
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:
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:
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:
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:
// 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:
// 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
// ❌ 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
// ✅ 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
// ❌ 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
// ✅ 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
// 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:
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
// Memoize expensive operations
const toolbox = useMemo(() => getDefaultToolbox(), []);
const workspace = useMemo(() => createWorkspace(toolbox), [toolbox]);
Future Enhancements
Input Port Detection
// Detect: Inputs["myInput"]
const inputRegex = /Inputs\["([^"]+)"\]/g;
Signal Output Detection
// 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
- Editor and runtime are SEPARATE windows - never forget this
- Pass context as function parameters - not via
this - Use Blockly v10+ API - check imports carefully
- Set explicit z-index - don't rely on DOM order
- Keep legacy and React separate - coordinate via events
- Initialize blocks before workspace - order matters
- Test with real user flow - early and often
- Document discoveries immediately - while context is fresh
References
Last Updated: 2026-01-12
Maintainer: Development Team
Status: Production-Ready Patterns