# 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
```
**The Fix:**
```html
```
**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 (
{/* Can't put canvas here - it's rendered by vanilla JS */}
);
}
```
### ✅ 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 ? (
{tabs.map((tab) => (
))}
) : 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