Building Blockly Integration From Scratch - Complete Beginner's Guide
What is Blockly? - Explained Much More Simply
Simple Definition:
Blockly is Google's library for creating visual block programming. Instead of typing code (like circle(r=50)), users drag and drop colorful blocks to create programs.
The LEGO Analogy - Explained:
Think of it like LEGO blocks for programming:
- Traditional coding: Like writing instructions with words (harder to understand)
- Blockly coding: Like building with LEGO blocks (easier to understand, visual)
How LEGO Blocks Work:
- You have different shaped blocks (circle blocks, rectangle blocks, etc.)
- Blocks snap together (they only fit certain ways)
- You build something by connecting blocks
- The result is something you can see and understand
How Blockly Works (Same Idea):
- You have different programming blocks (circle block, rectangle block, etc.)
- Blocks connect together (they only fit certain ways)
- You build a program by connecting blocks
- The result is code that runs
Visual Example:
Traditional Code (Text):
shape circle c1 { radius: 50 }
- Just text - have to remember syntax
- Easy to make typos
- Hard to understand structure
Blockly (Visual Blocks):
┌─────────────┐
│ Circle │ ← Drag this block
│ radius: 50 │ ← Fill in this field
└─────────────┘
- Visual blocks - see what you're doing
- Blocks guide you (can't make syntax errors)
- Easy to understand (visual structure)
Real-World Examples - Where Blockly is Used:
1. Scratch (MIT) - Programming for Kids
- Kids drag blocks to make characters move
- Instead of typing
moveForward(10), they drag a "move" block - Makes programming accessible to children
2. MIT App Inventor - Mobile App Building
- People drag blocks to create phone apps
- Instead of typing complex code, they connect blocks visually
- Lets non-programmers build apps
3. Code.org Tutorials - Learning to Code
- Students learn programming with blocks first
- After learning with blocks, they move to text code
- Blocks are the "training wheels" for programming
Why These Examples Matter:
All these tools use visual blocks because:
- Easier to learn - Visual is more intuitive than text
- Fewer mistakes - Blocks guide you, can't make syntax errors
- More accessible - Works for beginners and non-programmers
- Less intimidating - Blocks are friendlier than text code
The Key Insight:
Blockly turns programming from:
- "Writing instructions in a special language" (intimidating!)
- Into: "Building with colorful blocks" (friendly!)
Same result, different experience!
Why Add Visual Block Programming? - Much More Detailed Explanation
The Problem with Text Code - Explained Simply:
Text code can be intimidating and confusing for beginners. Let's break down why:
Example of Text Code:
shape circle c1 { radius: 50 }
Why This is Hard for Beginners:
1. Requires Learning Syntax:
- What is syntax? Syntax is the rules of how to write code correctly
- You have to remember:
shapecomes first, then the typecircle, then the namec1, then{ }brackets, etc. - Real-world analogy: Like learning grammar rules - you have to remember "subject verb object" order
2. Easy to Make Typos:
- Forget a bracket
{→ code breaks - Misspell
circleascircl→ code breaks - Wrong order → code breaks
- Real-world analogy: Like a typo in a password - one wrong character and nothing works
3. Hard to Remember Commands:
- What was the command for a circle?
shape?circle?drawCircle? - What parameters does a circle need?
radius?r?size? - Real-world analogy: Like remembering phone numbers - hard to remember exactly
4. No Visual Guidance:
- Text doesn't show you what's possible
- You have to know what commands exist
- Real-world analogy: Like typing in the dark - you don't know what's available
The Solution - Visual Blocks - Explained Simply:
Visual blocks solve ALL these problems:
1. Drag a "Circle" Block:
- What you do: Look for the circle block in the toolbox, drag it out
- Why this is better: You can SEE what's available - don't have to remember commands
- Visual guidance: You see all possible blocks, pick the one you want
- Real-world analogy: Like ordering from a menu - you see all options, pick what you want
2. Fill in the Radius with a Number Block:
- What you do: Drag a number block, connect it to the radius field
- Why this is better: Blocks only fit in the right places - can't put text where a number goes
- Visual constraints: The block shows you what goes where
- Real-world analogy: Like puzzle pieces - they only fit certain ways, so you can't make mistakes
3. No Syntax to Learn!
- What this means: You don't need to remember
shape circle c1 { radius: 50 }format - Why this is better: Blocks handle the syntax for you automatically
- Just drag and connect: That's it! No special rules to remember
- Real-world analogy: Like using a calculator vs. doing math by hand - the tool handles the rules
4. Visual Representation is Clearer:
- What this means: You can SEE the structure of your program
- Why this is better: Visual structure is easier to understand than text structure
- Easy to debug: See what connects to what at a glance
- Real-world analogy: Like a flowchart vs. written instructions - visual is clearer
Side-by-Side Comparison:
Text Code Approach:
Type: shape circle c1 { radius: 50 }
↓
Have to remember:
- "shape" keyword
- "circle" type
- "c1" name
- { } brackets
- "radius:" parameter name
- 50 value
- Proper spacing and order
If you forget ANY of this, it breaks!
Visual Blocks Approach:
See toolbox → Drag "Circle" block
↓
Block appears with fields
↓
See "radius:" field → Drag number block (50) into it
↓
Done! Block handles syntax automatically
Why Visual Blocks Win:
For Beginners:
- See what's possible (toolbox shows all blocks)
- Can't make syntax errors (blocks fit correctly or they don't)
- Don't need to remember commands (just look and drag)
- Visual structure is easier to understand
For Everyone:
- Faster to build (drag vs. type)
- Harder to make mistakes (blocks guide you)
- Easier to read (visual structure is clearer)
- More intuitive (building vs. writing)
The Key Insight:
Visual blocks turn programming from:
- "Remembering and typing commands correctly" (hard!)
- Into: "Building with visual blocks" (easy!)
Same result, MUCH easier process!
But We Want Both:
- Beginners can use blocks (easier)
- Power users can type code (faster)
- Both represent the same thing
- Both need to stay in sync
What Blockly Provides: Blockly is a JavaScript library that gives you:
- Drag-and-drop interface - Users drag blocks from a toolbox
- Code generation - Convert blocks to text code automatically
- Block creation from code - Convert text code back to blocks
- Customizable blocks - Define your own block types (circle, rectangle, etc.)
- Built-in UI - Handles connections, fields, toolboxes for you
Simple Analogy: Think of Blockly like a visual translator:
- Blocks = Visual representation (like a picture)
- Code = Text representation (like words)
- Blockly translates between them automatically
The Challenge: Two-Way Sync
The hard part isn't just adding Blockly - it's keeping blocks and text in sync:
When User Types Code:
- Code changes → Blocks should update automatically
- Example: Type
circle(r=50)→ Circle block appears
When User Moves a Block:
- Block changes → Code should update automatically
- Example: Change radius block to 75 → Code changes to
circle(r=75)
The Problem: If you're not careful, you get infinite loops:
- Code changes → Update blocks → Blocks trigger code change → Update blocks → Loop!
This requires careful event handling (covered in Chapter 07 on Bidirectional Systems) using sync flags and debouncing.
Why Blockly?
Some people learn better visually. Drag-and-drop blocks are less intimidating than text. But we also want power users to write code directly. So we need both - blocks AND text - and they need to stay in sync.
The Basic Setup
Blockly is a Google library that gives you drag-and-drop visual programming. You load it from CDN, inject it into a div, and it handles all the UI stuff.
Building Blockly Integration From Scratch
What You're Building: Integration with Google's Blockly library to provide visual block programming. This includes loading Blockly, creating a workspace, defining custom blocks, and implementing bidirectional sync with the text editor.
Why Blockly: Visual block programming makes coding more accessible, especially for beginners. Blockly provides drag-and-drop blocks, code generation, and a customizable interface. It integrates seamlessly with text-based code through bidirectional sync.
How to Build It Step by Step:
Step 1: Load Blockly from CDN
What You're Building: Loading Blockly library files from a CDN (Content Delivery Network). These are the JavaScript files that provide all Blockly functionality.
What is a CDN? CDN = Content Delivery Network. It's like a library on the internet - instead of downloading Blockly and bundling it yourself, you load it from Google's servers. Faster and easier!
Why Load from CDN:
- No bundling needed - Just add a
<script>tag - Always up-to-date - Get the latest version automatically
- Fast loading - Files are cached globally
- Easy to use - No npm install, no build step
How to Build It:
In index.html, add script tags BEFORE your application code. These files provide the foundation for Blockly integration.
Understanding Each File:
1. blockly_compressed.js - The Core Library:
- This is the main Blockly engine
- Handles workspace creation, block rendering, drag-and-drop
- Without this, nothing works!
- Think of it as the "operating system" for blocks
2. blocks_compressed.js - Standard Block Definitions:
- Pre-built blocks like "if/else", "for loops", "math operations"
- These are the basic building blocks
- You get these for free - don't need to define them yourself
- Think of it as a "standard library" of blocks
3. javascript_compressed.js - Code Generator:
- Converts blocks → JavaScript code
- Critical for generating code from visual blocks
- This is how blocks become text code
- Think of it as a "translator" (blocks → code)
4. msg/en.js - Language Strings:
- All the text labels ("if", "then", "repeat", etc.)
- Makes blocks readable in English
- For other languages, load different files (msg/es.js for Spanish, etc.)
- Think of it as the "dictionary" (what text to show)
Load Order Matters:
- Core library first (blockly_compressed.js)
- Then blocks (blocks_compressed.js) - needs core to exist
- Then generator (javascript_compressed.js) - needs blocks to exist
- Then language (msg/en.js) - needs everything to exist
<!-- Load core Blockly library (the "brain") -->
<script src="https://unpkg.com/blockly/blockly_compressed.js"></script>
<!-- Load standard block definitions (logic, loops, math, etc.) -->
<script src="https://unpkg.com/blockly/blocks_compressed.js"></script>
<!-- Load JavaScript code generator (converts blocks to code) -->
<script src="https://unpkg.com/blockly/javascript_compressed.js"></script>
<!-- Load English language strings (text labels) -->
<script src="https://unpkg.com/blockly/msg/en.js"></script>
If you see an error at this step:
Error: ReferenceError: Blockly is not defined
- What this means: Blockly library didn't load or loaded after your code runs
- Common causes:
- Script tags in wrong order: Your code runs before Blockly loads
- Script tags missing: Forgot to add the
<script>tags - CDN URL wrong: Typo in URL, file doesn't exist
- Network issue: Can't reach CDN (no internet, firewall blocking)
- Fix: Check script tags exist and are BEFORE your app code, verify URLs are correct, check network connection, add error handling:
if (typeof Blockly === 'undefined') { console.error('Blockly not loaded'); }
Error: Failed to load resource: the server responded with a status of 404
- What this means: CDN URL is wrong or file doesn't exist
- Common causes:
- Typo in URL:
blockly_compressed.jsmisspelled - Wrong version: URL points to version that doesn't exist
- CDN path changed: unpkg.com path structure changed
- Typo in URL:
- Fix: Verify URL in browser (paste URL in address bar, should download file), check Blockly docs for correct CDN URLs, try different CDN (cdnjs, jsdelivr)
Error: Scripts load but Blockly methods don't work
- What this means: Scripts loaded but Blockly object not available or wrong version
- Common causes:
- Missing script: One of the 4 scripts not loaded
- Wrong load order: Scripts loaded in wrong order (need core first)
- Version mismatch: Different versions of scripts (core vs blocks vs generator)
- Fix: Check all 4 scripts are loaded, verify load order (core → blocks → generator → msg), ensure same version for all scripts
Error: Blocks appear but text labels are missing/wrong
- What this means: Language file (msg/en.js) not loaded or loaded incorrectly
- Common causes:
- msg/en.js not included: Missing the language script tag
- Wrong language file: Loaded msg/es.js instead of msg/en.js (or vice versa)
- Load order wrong: msg file loaded before other scripts
- Fix: Ensure msg/en.js is loaded after other scripts, verify correct language file, check Blockly shows default text if language missing
Building This Step by Step:
- Open
index.html - Add script tag for
blockly_compressed.js(core library) - Add script tag for
blocks_compressed.js(standard blocks) - Add script tag for
javascript_compressed.js(code generator) - Add script tag for
msg/en.js(localization) - Load these before your application code
- These files provide the foundation for Blockly integration
Step 2: Create the Blockly Workspace
What You're Building: A Blockly workspace is like a canvas where users drag and drop blocks. It's the main visual interface for block programming.
Real-World Analogy: Think of the workspace like a whiteboard:
- Users drag blocks from the toolbox (the sidebar) onto the workspace (the whiteboard)
- They snap blocks together to build programs
- The workspace handles all the visual stuff (drawing, dragging, connections)
How to Build It:
First, you need a div in your HTML:
<div id="blocklyDiv" style="height: 600px; width: 100%;"></div>
Then in app.js, create the workspace:
Understanding Blockly.inject():
Blockly.inject()is the function that creates a workspace- First parameter:
'blocklyDiv'- The ID of the HTML div where Blockly appears - Second parameter:
{ ... }- Configuration object with settings
Important: The div must exist in your HTML BEFORE you call Blockly.inject()!
What is a Toolbox?
The toolbox is the sidebar where blocks live. Users drag blocks from here onto the workspace. It's like a toolbox of tools - each block is a different tool. TOOLBOX_XML is an XML string that defines which blocks appear in the toolbox.
For now: If TOOLBOX_XML doesn't exist yet, use:
toolbox: { kind: 'flyoutToolbox', contents: [] } // Empty toolbox for now
What the Grid Does: The grid is like graph paper - it provides visual alignment guides. When you drag a block, it "snaps" to grid positions, making everything look neat and organized.
Grid Settings Explained:
spacing: 20- Grid lines are 20 pixels apartlength: 3- Each grid line segment is 3px long (creates dotted line effect)colour: '#ccc'- Light gray color (subtle, not distracting)snap: true- Blocks automatically align to grid when dragged (like magnets!)
What Zoom Does: Zoom lets users zoom in (see details) or zoom out (see big picture). Essential for complex programs with many blocks.
controls: true- Show zoom buttons (+ and -) in the workspace UIwheel: true- Allow mouse wheel scrolling to zoom (convenient!)
What is a Renderer? The renderer controls how blocks LOOK visually - colors, shapes, shadows, rounded corners, etc. It's like choosing a theme.
'thrasos'- Modern, clean style (recommended)'geras'- Classic style (older look)'zelos'- Modern alternative style
// Blockly.inject() creates a new Blockly workspace
// First parameter: ID of the div where Blockly should be rendered
// Second parameter: Configuration object with settings
blocklyWorkspace = Blockly.inject('blocklyDiv', {
// XML string defining what blocks appear in the sidebar
toolbox: TOOLBOX_XML,
// Visual grid overlay on the workspace (like graph paper)
grid: {
spacing: 20, // Distance between grid lines in pixels
length: 3, // Length of grid line segments (creates dotted effect)
colour: '#ccc', // Color of grid lines (light gray)
snap: true // Snap blocks to grid when dragging
},
// Zoom controls and mouse wheel zooming
zoom: {
controls: true, // Show zoom in/out buttons in the UI
wheel: true // Allow mouse wheel to zoom
},
// Visual style for blocks ('thrasos' is modern and clean)
renderer: 'thrasos'
});
});
After injection:
blocklyWorkspace is now a Blockly.Workspace object. You can use it to:
- Add blocks programmatically
- Get code from blocks
- Listen to block changes
- Manipulate the workspace
If you see an error at this step:
Error: TypeError: Blockly.inject is not a function
- What this means: Blockly library not loaded or wrong version
- Common causes:
- Blockly scripts not loaded: Missing script tags in HTML
- Scripts loaded after this code runs: Load order wrong
- Wrong Blockly version: Using old version that doesn't have
injectmethod
- Fix: Check Blockly scripts are loaded BEFORE this code, verify
typeof Blockly !== 'undefined', check Blockly version
Error: Error: Blockly workspace div not found: blocklyDiv
- What this means: HTML div with id "blocklyDiv" doesn't exist
- Common causes:
- Div not in HTML: Missing
<div id="blocklyDiv">in HTML - Wrong ID: Div has different ID like
blockly-divorblocksDiv - Script runs before HTML loads: DOM not ready yet
- Div not in HTML: Missing
- Fix: Add div to HTML:
<div id="blocklyDiv" style="height:600px;"></div>, verify ID matches exactly (case-sensitive), useDOMContentLoadedevent or put script at end of body
Error: ReferenceError: TOOLBOX_XML is not defined
- What this means: TOOLBOX_XML variable doesn't exist
- Common causes:
- Variable not defined: Forgot to create TOOLBOX_XML
- Wrong variable name:
toolboxXmlvsTOOLBOX_XML(case mismatch) - Variable in wrong scope: Defined in different file, not accessible
- Fix: Define toolbox or use simple one:
toolbox: { kind: 'flyoutToolbox', contents: [] }, or create TOOLBOX_XML variable with XML string
Error: Workspace appears but blocks can't be dragged/connected
- What this means: Toolbox empty or blocks not defined
- Common causes:
- Empty toolbox:
contents: []means no blocks available - Blocks not registered: Custom blocks defined but not registered with Blockly
- Toolbox XML invalid: XML syntax error in toolbox definition
- Empty toolbox:
- Fix: Add blocks to toolbox, ensure blocks are registered with
Blockly.defineBlocksWithJsonArray(), check toolbox XML syntax
Error: Blocks appear but look wrong (colors, shapes incorrect)
- What this means: Renderer issue or block definitions wrong
- Common causes:
- Renderer not supported: Using renderer name that doesn't exist
- Block definitions incomplete: Missing color/style properties in block definitions
- CSS conflicts: Your CSS overriding Blockly styles
- Fix: Use valid renderer name ('thrasos', 'geras', 'zelos'), check block definitions include style properties, check for CSS conflicts
Understanding the Configuration: Each configuration option serves a specific purpose:
toolbox- This is the most important setting. It defines what blocks users can access. Without a toolbox, users can't create any blocks. The XML format is covered in detail later, but it's essentially a structured list of block categories and types.grid- The visual grid is optional but helpful. It provides visual alignment guides and makes the workspace look more professional. Thesnap: trueoption is particularly useful - it automatically aligns blocks to grid positions, making the workspace neater.zoom- Zoom controls are essential for complex programs. Users need to zoom in to see details and zoom out to see the big picture. Both button controls and mouse wheel zooming provide convenient ways to adjust the view.renderer- The renderer affects the visual appearance of blocks. Different renderers have different aesthetics. 'thrasos' is a modern, clean style that looks professional.
The DOM Element:
The 'blocklyDiv' is a div element in your HTML where Blockly will be rendered. It should be an empty div (or mostly empty) because Blockly will populate it with its UI. Make sure this div exists in your HTML before calling Blockly.inject().
Step 3: Define Custom Blocks
What You're Building: Custom Blockly blocks that represent AQUI language constructs (shapes, parameters, etc.). Each block needs a definition (appearance) and a code generator (how to convert to AQUI code).
Why Custom Blocks: Blockly comes with standard blocks (logic, loops, math), but we need blocks specific to AQUI (shapes, parameters). Custom blocks make the language accessible through visual programming.
How to Build It:
We do this in blocks-umd.js. This file is UMD format (not ES module) because it's loaded via <script> tag and needs to work with Blockly in the global scope.
Building Block Definitions From Scratch
What You're Building: Block definitions tell Blockly how to create and display your custom blocks. It's like a blueprint - you describe what the block looks like, what inputs it has, and how it behaves.
Real-World Analogy: Think of it like describing a LEGO block:
- What does it look like? (visual appearance)
- What can connect to it? (inputs/outputs)
- What text does it show? (labels)
- What color is it? (styling)
Why Block Definitions: Blockly needs to know:
- What blocks exist (circle, rectangle, etc.)
- How they look (text, colors, shapes)
- What inputs they accept (text fields, number fields, other blocks)
- How to render them (visual appearance)
Without definitions, Blockly doesn't know your custom blocks exist!
Simple Example:
Blockly.defineBlocksWithJsonArray([{
type: 'aqui_shape_circle',
message0: 'circle %1',
args0: [{ type: 'field_input', name: 'NAME', text: 'c1' }],
colour: 160 // Orange color
}]);
This creates a block that:
- Shows "circle [text field]" on screen
- Has a text input field (user types shape name like "c1")
- Is colored orange
- Has type 'aqui_shape_circle' (unique identifier)
How to Build It Step by Step:
Step 3.1: Define the Block Structure
Use Blockly.defineBlocksWithJsonArray() to register blocks:
What is a Block Type?
The type is a unique name that identifies this block. It's like an ID - no two blocks should have the same type.
Naming Convention:
'aqui_'prefix - Identifies these as AQUI language blocks (not standard Blockly blocks)'shape'- Category (this is a shape block)'circle'- Specific block (circle shape)
So 'aqui_shape_circle' means "AQUI language, shape category, circle block"
What is message0?
message0 is the text that appears on the block. The %1, %2, etc. are placeholders for inputs (like text fields or other blocks).
Understanding Placeholders - Detailed Breakdown:
Blockly uses a special placeholder syntax in message strings. Placeholders are numbered starting from 1:
%1= First input (will be replaced with actual input field or connected block)%2= Second input%3= Third input- And so on...
How Placeholders Work:
When Blockly renders a block, it:
- Reads the
message0string (e.g.,'shape circle %1 { radius: %2 }') - Finds placeholders (
%1,%2, etc.) - Looks in
args0array to find what each placeholder represents - Replaces placeholders with the actual input fields/blocks
- Renders the final block
Detailed Example Step-by-Step:
Let's trace through a complete example:
Step 1: Define the Message Template
message0: 'shape circle %1 { radius: %2 }'
This string contains:
- Text:
'shape circle ' - Placeholder:
%1(first input) - Text:
' { radius: ' - Placeholder:
%2(second input) - Text:
' }'
Step 2: Define What Each Placeholder Represents
args0: [
{ type: 'field_input', name: 'NAME', text: 'c1' }, // %1 = text input for name
{ type: 'field_number', name: 'RADIUS', value: 50 } // %2 = number input for radius
]
Step 3: Blockly Processes and Renders
Processing Phase:
- Blockly reads
message0:'shape circle %1 { radius: %2 }' - Finds
%1placeholder - Looks at
args0[0]→{ type: 'field_input', name: 'NAME', text: 'c1' } - Creates a text input field with default value "c1"
- Finds
%2placeholder - Looks at
args0[1]→{ type: 'field_number', name: 'RADIUS', value: 50 } - Creates a number input field with default value 50
Rendering Phase: Blockly replaces placeholders with actual UI elements:
'shape circle '→ Text label "shape circle "%1→ Text input field showing "c1" (user can edit)' { radius: '→ Text label " { radius: "%2→ Number input field showing "50" (user can edit)' }'→ Text label " }"
Final Rendered Block:
┌─────────────────────────────┐
│ shape circle [c1] { radius: │
│ [50] } │
└─────────────────────────────┘
Why Placeholders Are Numbered Starting from 1:
This follows Blockly's convention. The numbering corresponds to the args0 array indices:
%1corresponds toargs0[0](first element, index 0)%2corresponds toargs0[1](second element, index 1)%3corresponds toargs0[2](third element, index 2)
Note: The placeholder number (1-based) is offset by 1 from the array index (0-based). This is why %1 uses args0[0], not args0[1].
Multiple Message Strings:
Blocks can have multiple message strings (message0, message1, message2, etc.) for multi-line display:
message0: 'shape circle %1',
message1: 'radius: %2',
message2: 'color: %3'
This creates a block with three lines:
┌─────────────────┐
│ shape circle %1 │ ← message0
│ radius: %2 │ ← message1
│ color: %3 │ ← message2
└─────────────────┘
Input Field Types:
Different placeholder types create different input fields:
Text Input (field_input):
{ type: 'field_input', name: 'NAME', text: 'default' }
- Creates a text field where users can type
name: Internal identifier for accessing the valuetext: Default text value
Number Input (field_number):
{ type: 'field_number', name: 'RADIUS', value: 50 }
- Creates a number field (may have up/down arrows)
name: Internal identifiervalue: Default numeric value
Dropdown (field_dropdown):
{ type: 'field_dropdown', name: 'SHAPE_TYPE', options: [['circle', 'CIRCLE'], ['rectangle', 'RECT']] }
- Creates a dropdown menu
name: Internal identifieroptions: Array of[display_text, value]pairs
Block Connection (input_value, input_statement):
{ type: 'input_value', name: 'EXPRESSION' }
- Creates a connection point where another block can attach
name: Internal identifier for the connected block
Complete Example with All Details:
Blockly.defineBlocksWithJsonArray([{
type: 'aqui_shape_circle',
// Message template with placeholders
message0: 'shape circle %1 { radius: %2 }',
// Define what each placeholder represents
args0: [
// %1 = Text input for shape name
{
type: 'field_input', // Creates a text input field
name: 'NAME', // Internal name (used to access value later)
text: 'c1' // Default value shown in field
},
// %2 = Number input for radius
{
type: 'field_number', // Creates a number input field
name: 'RADIUS', // Internal name
value: 50, // Default numeric value
min: 0, // Optional: minimum value
max: 1000, // Optional: maximum value
precision: 1 // Optional: decimal places (1 = allow 50.5, etc.)
}
],
// Visual styling
colour: 160, // Orange color (0-360 hue value)
// Behavior
previousStatement: null, // Can connect above this block
nextStatement: null // Can connect below this block
}]);
Accessing Field Values Later:
When generating code from blocks, you access field values using the name property:
const blockName = block.getFieldValue('NAME'); // Gets text from NAME field → "c1"
const radius = block.getFieldValue('RADIUS'); // Gets number from RADIUS field → 50
This is why the name property is important - it's how you retrieve the user's input when generating code.
Block Structure:
type: Unique identifier, used to reference the blockmessage0/1: Text templates that define what the block saysargs0/1: Define what the placeholders (%1, %2) representpreviousStatement/nextStatement: Allow blocks to stack verticallycolour: Visual indicator of block category
// Define the block using JSON
// Blockly.defineBlocksWithJsonArray() registers blocks with Blockly
Blockly.defineBlocksWithJsonArray([{
// Unique identifier for this block type ('aqui_' prefix + category + name)
type: 'aqui_shape_circle',
// First line of text on the block (%1 is placeholder for first input)
message0: 'shape circle %1',
// Define inputs for message0 (text input field for shape name)
args0: [{ type: 'field_input', name: 'NAME', text: 'c1' }],
// Second message line for properties (%1 will be replaced with statement input)
message1: '%1',
// Statement input - other blocks can be connected here (property blocks)
args1: [{ type: 'input_statement', name: 'PROPS' }],
// Allow stacking with other blocks (attach above and below)
previousStatement: null,
nextStatement: null,
// Block background color (Green for shapes)
colour: '#5CA65C'
}]);
Step 3.2: Define the Code Generator
The generator function is called when Blockly needs to convert blocks to code. It extracts values from the block and formats them as AQUI code. The function must return a string that matches AQUI syntax.
// Define the code generator function
// Blockly.JavaScript['block_type'] is how you register a code generator
Blockly.JavaScript['aqui_shape_circle'] = function(block) {
// Get the shape name from the NAME field (trim removes whitespace)
const name = block.getFieldValue('NAME').trim();
// Collect code from connected property blocks
// collectLinesUnique_() walks through blocks connected to PROPS input
const body = collectLinesUnique_(block, 'PROPS');
// Generate AQUI code string matching the syntax: shape circle c1 { ... }
return `shape circle ${name} {\n${body}\n}\n`;
};
The Complete Block Definition:
// Define the block
Blockly.defineBlocksWithJsonArray([{
type: 'aqui_shape_circle',
message0: 'shape circle %1',
args0: [{ type: 'field_input', name: 'NAME', text: 'c1' }],
message1: '%1',
args1: [{ type: 'input_statement', name: 'PROPS' }],
previousStatement: null,
nextStatement: null,
colour: '#5CA65C' // Green for shapes
}]);
// Define the code generator
Blockly.JavaScript['aqui_shape_circle'] = function(block) {
const name = block.getFieldValue('NAME').trim();
const body = collectLinesUnique_(block, 'PROPS');
return `shape circle ${name} {\n${body}\n}\n`;
};
Building This Step by Step:
- Create
blocks-umd.jsfile (UMD format for script tag loading) - Call
Blockly.defineBlocksWithJsonArray()with array of block definitions - Define block type with unique identifier
- Set message0 with text template and placeholders (%1, %2)
- Define args0 array with input field definitions
- Set message1 for additional block lines
- Define args1 array with statement input
- Set previousStatement and nextStatement to null (allow stacking)
- Set colour for visual category
- Register code generator function as
Blockly.JavaScript['block_type'] - Get field values using
getFieldValue() - Collect code from connected blocks using helper function
- Return formatted AQUI code string
- This creates a custom block that generates AQUI code
What's happening:
message0is the first line of the block.%1is a placeholder for the first input (the name field)args0defines that input - it's a text field where you type the shape namemessage1andargs1add a second section where you can attach property blockspreviousStatementandnextStatementmean this block can stack with others- The code generator function takes the block and returns AQUI code as a string
Building the collectLinesUnique_ Helper Function:
What You're Building: A helper function that walks through a chain of connected blocks and collects their generated code. This is used to generate the body of shape definitions (properties like radius, color, etc.).
Why This Function: When blocks are connected together (like property blocks attached to a shape block), we need to collect code from all of them. This function traverses the chain and generates code for each block.
How to Build It Step by Step:
Why This Function: When blocks are connected together (like property blocks attached to a shape block), we need to collect code from all of them. This function traverses the chain and generates code for each block.
Why This Approach:
- Starts with first connected block and follows the chain
- Generates code for each block using its registered generator
- Indents each line with 2 spaces (for nested structure)
- Joins lines with newlines for readable output
Example:
If you have blocks connected like: radius: 50 → color: red → fill: true
This function generates:
radius: 50
color: red
fill: true
function collectLinesUnique_(blk, input = 'STACK') {
// Initialize lines array to store generated code from each connected block
const lines = [];
// Get the first block connected to the specified input
// Default input name is 'STACK' (for statement inputs)
let child = blk.getInputTargetBlock(input);
// Loop through all connected blocks (blocks are connected in a chain)
while (child) {
// Get the code generator for this block type
const gen = Blockly.JavaScript[child.type];
// Generate code for this block (if generator exists and is a function)
// trim() removes leading/trailing whitespace
const raw = typeof gen === 'function' ? gen(child).trim() : '';
// Add generated code to lines array with 2-space indentation
if (raw) lines.push(' ' + raw);
// Move to next connected block
child = child.getNextBlock();
}
// Join all lines with newlines (creates multi-line string)
return lines.join('\n');
}
Building This Step by Step:
- Create function
collectLinesUnique_()with block and input parameters - Initialize empty lines array
- Get first connected block using
getInputTargetBlock() - Add while loop that continues while child exists
- Get code generator function for child block type
- Check if generator is a function
- If yes, call generator with child block and trim result
- If code generated, add to lines array with 2-space indentation
- Move to next block using
getNextBlock() - After loop, join all lines with newlines
- Return joined string
- This function collects code from connected block chains
The Toolbox
The toolbox is an XML string that defines what blocks appear in the sidebar. It's in app.js as TOOLBOX_XML:
<xml>
<category name="Shapes" colour="#5CA65C">
<block type="aqui_shape_circle"/>
<block type="aqui_shape_rectangle"/>
<!-- etc -->
</category>
<category name="Parameters" colour="#CE5C81">
<block type="aqui_param">
<value name="NAME">
<shadow type="text"><field name="TEXT">size</field></shadow>
</value>
</block>
</category>
</xml>
Each <block> tag adds a block to the toolbox. The shadow blocks are default values that appear when you drag a block out.
Bidirectional Sync
This is the tricky part. When you change blocks, we need to update the text editor. When you change text, we need to rebuild the blocks. And we need to prevent infinite loops.
Building Blocks → Text Sync From Scratch:
What You're Building: A change listener that detects when Blockly blocks are modified and synchronizes those changes to the text editor. This listener must filter events carefully and use sync flags to prevent infinite loops.
Why This Listener: When users move or modify blocks, the text code needs to update to reflect those changes. But we must prevent loops: if text is updating blocks, we shouldn't sync back to text. This listener implements that logic.
How to Build It Step by Step:
What This Listener Does: Every time ANYTHING changes in the Blockly workspace, this function runs. It needs to:
- Filter out unnecessary events (UI events, field changes during rebuild)
- Generate code from blocks
- Update the text editor (with sync flag to prevent loops)
Understanding Event Filtering: Not all events should trigger code updates. We filter out events that don't actually change the code structure:
- UI Events - Just dragging blocks around (doesn't change code structure)
- Field Changes During Sync - Field changes fire separate events, and we only want to sync when the structure actually changes
- Already Syncing - If
syncingFromBlocksis true, we're already in the process of syncing (prevent re-entrancy)
blocklyWorkspace.addChangeListener(event => {
// Filter out UI-only events and field changes during sync
if (event.type === Blockly.Events.UI ||
(event.type === Blockly.Events.CHANGE && event.element === 'field') ||
syncingFromBlocks) {
return; // Skip this event - doesn't need code update
}
try {
// Generate code from blocks using code generators we defined
const code = Blockly.JavaScript.workspaceToCode(blocklyWorkspace);
// Only update editor if code actually changed (avoid unnecessary updates)
if (editor.getValue() !== code) {
// Set sync flag to prevent loops (blocks are updating editor, don't sync back)
syncingFromBlocks = true;
// Update editor atomically (wraps operations in one transaction)
// This prevents intermediate change events from firing
editor.operation(() => {
// Step 6: Set editor content to generated code
editor.setValue(code);
// Step 7: Execute the code to update canvas
// This runs the interpreter and updates shapes
runCode();
});
}
} catch (e) {
// Step 8: Handle code generation errors
// If code generation fails (invalid blocks, etc.), log warning
// Don't crash - just warn and continue
console.warn('Codegen error:', e);
} finally {
// Step 9: Always clear sync flag
// This ensures flag is reset even if error occurs
syncingFromBlocks = false;
}
});
Why This Approach:
- Filters events to only handle structural changes
- Checks sync flag to prevent loops
- Uses
editor.operation()for atomic updates - Generates code only when blocks actually change
- Always clears flag in finally block
Building This Step by Step:
- Call
blocklyWorkspace.addChangeListener()with event handler - Check if event is UI-only, return early if so
- Check if event is field change, return early if so
- Check
syncingFromBlocksflag, return early if set - Wrap in try-catch for error handling
- Generate code using
Blockly.JavaScript.workspaceToCode() - Compare generated code with current editor content
- If different, set
syncingFromBlocksflag to true - Use
editor.operation()to wrap editor updates - Call
editor.setValue()with generated code - Call
runCode()to execute and update canvas - In catch block, log warning for errors
- In finally block, clear
syncingFromBlocksflag - This listener syncs blocks → text while preventing loops
How it works:
- Blockly fires a change event
- We check if it's a real change (not just UI stuff)
- We check the
syncingFromBlocksflag - if we're already syncing, ignore it - Generate code from blocks using
Blockly.JavaScript.workspaceToCode() - If it's different from what's in the editor, update the editor
- Run the code to update the canvas
- Clear the flag
Text → Blocks:
This is harder. We need to parse the AQUI code, build an AST, then convert AST nodes to Blockly blocks.
function rebuildWorkspaceFromAqui(code, workspace) {
// Parse the code
const ast = new Parser(new Lexer(code)).parse();
// Disable events during rebuild (prevents sync loops)
Blockly.Events.disable();
try {
workspace.clear();
let cursorY = 10;
ast.forEach(stmt => {
const blk = stmtToBlock(stmt, workspace);
if (blk) {
blk.moveBy(10, cursorY);
cursorY += blk.getHeightWidth().height + 25;
}
});
} finally {
Blockly.Events.enable();
}
workspace.render();
}
The stmtToBlock function:
This converts AST nodes to Blockly blocks. It's recursive - expressions become blocks, statements become blocks, etc.
function stmtToBlock(stmt, workspace) {
if (stmt.type === 'shape') {
const blk = workspace.newBlock(`aqui_shape_${stmt.shapeType}`);
blk.setFieldValue(stmt.name, 'NAME');
// Convert properties to blocks and connect them
let prevBlock = null;
for (const [key, value] of Object.entries(stmt.params)) {
const propBlock = workspace.newBlock('aqui_prop_expr');
propBlock.setFieldValue(key, 'KEY');
const valueBlock = exprToBlock(value, workspace);
if (valueBlock) {
propBlock.getInput('VAL').connection.connect(valueBlock.outputConnection);
}
if (prevBlock) {
prevBlock.nextConnection.connect(propBlock.previousConnection);
} else {
blk.getInput('PROPS').connection.connect(propBlock.previousConnection);
}
prevBlock = propBlock;
}
return blk;
}
// ... handle other statement types
}
The exprToBlock function:
This converts expressions (numbers, identifiers, binary ops, etc.) to blocks:
function exprToBlock(expr, workspace) {
if (expr.type === 'number') {
const blk = workspace.newBlock('math_number');
blk.setShadow(true); // Shadow blocks can't be disconnected
blk.setFieldValue(String(expr.value), 'NUM');
return blk;
}
if (expr.type === 'identifier') {
// Check if it's a parameter
if (_PARAMS.has(expr.name)) {
const blk = workspace.newBlock('aqui_param_get');
blk.setFieldValue(expr.name, 'NAME');
return blk;
}
// Otherwise it's just text
const blk = workspace.newBlock('text');
blk.setFieldValue(expr.name, 'TEXT');
return blk;
}
if (expr.type === 'binary_op') {
const blk = workspace.newBlock('math_arithmetic');
blk.setFieldValue(expr.operator.toUpperCase(), 'OP');
const leftBlock = exprToBlock(expr.left, workspace);
const rightBlock = exprToBlock(expr.right, workspace);
blk.getInput('A').connection.connect(leftBlock.outputConnection);
blk.getInput('B').connection.connect(rightBlock.outputConnection);
return blk;
}
// ... handle other expression types
}
Important details:
- Shadow blocks (
setShadow(true)) are default values that can't be disconnected. Use them for literals. - Connections use
outputConnectionandinput.connection. Blockly handles the visual wiring. - You need to call
blk.initSvg()andblk.render()after creating blocks, or they won't appear.
Preventing Sync Loops
The syncingFromBlocks flag is crucial. Without it:
- User changes blocks
- Blocks → Text sync fires
- Text editor updates
- Text → Blocks sync fires (if enabled)
- Blocks update
- Blocks → Text sync fires again
- Infinite loop
We prevent this by:
- Setting
syncingFromBlocks = truebefore updating text - Checking the flag before syncing
- Disabling Blockly events during bulk operations (
Blockly.Events.disable())
Code Generation
Blockly's code generator works by calling functions stored in Blockly.JavaScript. When you call Blockly.JavaScript.workspaceToCode(), it:
- Finds the top-level blocks (blocks not connected to anything above)
- For each block, calls
Blockly.JavaScript[block.type](block) - Concatenates the results
Your code generator functions should return strings. Blockly handles the rest.
Gotcha: The code generator needs to return valid AQUI code. If your generator returns malformed code, the parser will fail when you try to sync back.
Mode Switching
We have a toggle button that switches between text and blocks mode:
document.getElementById('toggle-editor-mode').addEventListener('click', () => {
if (editorMode === 'text') {
// Switch to blocks
editorMode = 'blocks';
textContainer.style.display = 'none';
blocklyContainer.style.display = 'block';
updateBlocksFromText(); // Rebuild blocks from current text
refreshBlockly();
} else {
// Switch to text
editorMode = 'text';
blocklyContainer.style.display = 'none';
textContainer.style.display = 'flex';
editor.refresh();
runCode(); // Execute current code
}
});
When switching to blocks, we rebuild the workspace from the current text. When switching to text, we just show the editor (the code is already there from the last sync).
Common Issues
Blocks don't appear:
- Make sure
blocks-umd.jsloads after Blockly - Check that blocks are defined before the workspace is created
- Verify the toolbox XML references the block types correctly
Sync loops:
- Check the
syncingFromBlocksflag - Make sure you're disabling events during bulk operations
- Verify code generators return valid AQUI code
Code generation fails:
- Check that code generators are registered in
Blockly.JavaScript - Make sure generator functions handle all block states (missing inputs, etc.)
- Test with simple blocks first
Text → Blocks conversion fails:
- Check that the AST is valid
- Verify
stmtToBlockhandles all statement types - Make sure
exprToBlockhandles all expression types - Check for circular references in the AST
The bidirectional sync is the hardest part. Get the flags right, handle edge cases, and test thoroughly. Once it works, it's pretty solid.
Implementation Adjustments and Notes
This section documents adjustments that were needed after the initial implementation, based on practical experience. These are specific fixes and additions that were required to make Blockly work correctly in the actual application.
Block Definitions Must Come First
Issue: Blockly blocks must be defined before the workspace is created. Initial implementation tried to create the workspace first, but Blockly couldn't find the block definitions, resulting in an empty toolbox or errors.
Why This Order Matters:
Blockly works by registering block types in a global registry when Blockly.defineBlocksWithJsonArray() is called. When you create a workspace using Blockly.inject(), Blockly needs to know which block types exist so it can:
- Populate the toolbox: The toolbox XML references block types (e.g.,
type="aqui_shape_circle"). Blockly looks up these types in its registry to know what blocks to show. - Create block instances: When users drag blocks from the toolbox, Blockly needs the block definition to know how to render them.
- Generate code: When converting blocks to code, Blockly looks up the code generator function using the block type as a key.
What Happens If Definitions Come After Workspace Creation:
If you try to create the workspace before defining blocks:
1. Call Blockly.inject() → Workspace created
2. Toolbox tries to load blocks → Looks up block types in registry
3. Registry is empty! (blocks not defined yet)
4. Result: Empty toolbox or errors like "Block type 'aqui_shape_circle' not found"
Blockly can't show blocks it doesn't know about, so the toolbox appears empty or broken.
What Happens With Correct Order:
When definitions come before workspace creation:
1. Call defineBlocks() → Blockly.defineBlocksWithJsonArray() registers block types
→ Registry now contains: 'aqui_shape_circle', 'aqui_shape_rectangle', etc.
2. Call Blockly.inject() → Workspace created
3. Toolbox tries to load blocks → Looks up block types in registry
4. Registry has the blocks! ✓
5. Result: Toolbox displays all blocks correctly
The Registration Process:
When you call Blockly.defineBlocksWithJsonArray([{type: 'aqui_shape_circle', ...}]):
- Blockly parses the JSON array
- For each block definition, it:
- Stores the block structure (appearance, inputs, etc.) in an internal registry
- Creates a mapping:
blockType→blockDefinition
- Later, when the workspace needs a block, it looks up the type in this registry
Why Code Generators Must Also Come Before:
Code generators (Blockly.JavaScript['block_type']) are also registered in a global registry. When Blockly.workspaceToCode() is called, it:
- Looks up each block's code generator function using its type
- Calls that function to generate code
- If the generator isn't registered, it throws an error or returns empty string
What Was Added: A complete defineBlocks() method that registers custom Blockly blocks using Blockly.defineBlocksWithJsonArray() and their corresponding code generators using Blockly.JavaScript['block_type']. This method is called BEFORE Blockly.inject() to ensure all blocks are registered before the workspace tries to use them.
Specific Implementation:
defineBlocks() {
if (typeof Blockly === 'undefined') return;
// Define basic shape blocks with complete JSON structure
Blockly.defineBlocksWithJsonArray([
{
"type": "aqui_shape_circle",
"message0": "circle %1 radius: %2",
"args0": [
{
"type": "field_input",
"name": "NAME",
"text": "c1"
},
{
"type": "field_number",
"name": "RADIUS",
"value": 50,
"min": 1
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 160,
"tooltip": "Create a circle shape"
},
{
"type": "aqui_shape_rectangle",
"message0": "rectangle %1 width: %2 height: %3",
"args0": [
{
"type": "field_input",
"name": "NAME",
"text": "r1"
},
{
"type": "field_number",
"name": "WIDTH",
"value": 100,
"min": 1
},
{
"type": "field_number",
"name": "HEIGHT",
"value": 50,
"min": 1
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 160,
"tooltip": "Create a rectangle shape"
}
]);
// Define code generators for each block type
Blockly.JavaScript['aqui_shape_circle'] = function(block) {
const name = block.getFieldValue('NAME').trim() || 'c1';
const radius = block.getFieldValue('RADIUS') || 50;
return `shape circle ${name} { radius: ${radius} }\n`;
};
Blockly.JavaScript['aqui_shape_rectangle'] = function(block) {
const name = block.getFieldValue('NAME').trim() || 'r1';
const width = block.getFieldValue('WIDTH') || 100;
const height = block.getFieldValue('HEIGHT') || 50;
return `shape rectangle ${name} { width: ${width}, height: ${height} }\n`;
};
}
Constructor Order Fix:
constructor(editor, interpreter, shapeManager) {
// ... initialization ...
// CRITICAL: Define blocks FIRST, before workspace creation
this.defineBlocks();
// Then setup workspace (which references the blocks we just defined)
this.setupWorkspace();
this.setupSync();
}
Why This Was Necessary: Blockly's inject() method creates the workspace and toolbox immediately. If block types are referenced in the toolbox XML/config but haven't been registered yet, Blockly throws errors like "Unknown block type: aqui_shape_circle". By defining blocks first, we ensure they exist when Blockly tries to populate the toolbox.
What Happened Without This: The toolbox would appear empty, or Blockly would throw JavaScript errors in the console, preventing the workspace from functioning.
Workspace Initialization Timing
Issue: Blockly workspace needs proper container sizing, even when initially hidden. The initial implementation tried to initialize Blockly in a hidden container, but Blockly requires dimensions to calculate its internal layout. When the container has display: none, getBoundingClientRect() returns zero dimensions, causing Blockly to fail silently or render incorrectly.
What Was Added: Dimension checking and fallback sizing before workspace initialization.
Specific Implementation:
setupWorkspace() {
// ... find blocklyDiv ...
// CRITICAL ADDITION: Check if container has valid dimensions
const container = blocklyDiv.parentElement;
if (container) {
const rect = container.getBoundingClientRect();
// If container is hidden (zero dimensions), set explicit size
if (rect.width === 0 || rect.height === 0) {
// Container is hidden with display: none
// Blockly needs dimensions to initialize, so set them explicitly
blocklyDiv.style.width = '100%';
blocklyDiv.style.height = '600px';
// Also ensure parent container has size if hidden
if (container.style.display === 'none') {
// Temporarily make it visible for dimension calculation, or use fixed size
// We use fixed size approach to avoid flicker
}
}
}
// Now initialize workspace - Blockly will use the dimensions we set
this.workspace = Blockly.inject(blocklyDiv, injectOptions);
}
Additional Change - Container Structure: The HTML structure was modified to support proper sizing:
<!-- Initial structure (didn't work for hidden initialization) -->
<div id="blocklyDiv" style="display: none;"></div>
<!-- Updated structure (works for hidden initialization) -->
<div class="editor-container" id="blockly-editor-container" style="display: none;">
<div id="blocklyDiv"></div>
</div>
CSS Addition for Proper Sizing:
#blockly-editor-container {
flex: 1;
overflow: hidden;
position: relative; /* Added: Allows absolute positioning of Blockly div */
}
#blocklyDiv {
position: absolute; /* Added: Allows Blockly to fill container */
top: 0;
left: 0;
height: 100%;
width: 100%;
}
Why This Was Necessary: Blockly internally calculates SVG dimensions and workspace layout based on container size. When a container is hidden (display: none), the browser doesn't calculate its layout, so getBoundingClientRect() returns all zeros. Blockly then creates a workspace with zero size, which breaks rendering and interaction. By ensuring dimensions exist (even if the container is hidden), Blockly can initialize correctly.
What Happened Without This: Blockly would initialize but the workspace would be invisible or non-functional. SVG elements would have zero dimensions, blocks couldn't be dragged, and the toolbox wouldn't render properly. When switching to Blockly mode, users would see a blank area.
Toggle Between Text and Blockly Editors
Issue: The initial implementation treated the Blocks button as a simple show/hide toggle for Blockly, but this caused problems:
- Both editors could be visible simultaneously
- Blockly wouldn't resize when shown
- Button label didn't reflect current state
- No proper cleanup when switching modes
What Was Added: Complete toggle implementation with container swapping, resize handling, and button label updates.
Specific Implementation:
toggleVisibility() {
const textEditorContainer = document.getElementById('text-editor-container');
const blocklyContainer = document.getElementById('blockly-editor-container');
const blocklyDiv = document.getElementById('blocklyDiv');
if (!textEditorContainer || !blocklyContainer) return false;
// Determine current state
const isTextVisible = textEditorContainer.style.display !== 'none';
if (isTextVisible) {
// SWITCH TO BLOCKLY MODE
// Step 1: Hide text editor
textEditorContainer.style.display = 'none';
// Step 2: Show Blockly container with proper flex layout
blocklyContainer.style.display = 'flex';
blocklyContainer.style.flex = '1';
// Step 3: Ensure Blockly div has proper dimensions
// This is critical - Blockly needs actual pixel dimensions
if (blocklyDiv) {
const containerRect = blocklyContainer.getBoundingClientRect();
// Set explicit dimensions based on actual container size
blocklyDiv.style.width = containerRect.width + 'px';
blocklyDiv.style.height = containerRect.height + 'px';
}
// Step 4: Initialize workspace if not already done
// This handles the case where user switches to Blockly before it's initialized
if (!this.workspace && typeof Blockly !== 'undefined') {
this.setupWorkspace();
}
// Step 5: Resize Blockly after DOM update completes
// Multiple timeouts ensure resize happens after browser layout
const resizeBlockly = () => {
if (this.workspace && typeof Blockly !== 'undefined') {
try {
Blockly.svgResize(this.workspace);
} catch (error) {
console.error('Error resizing Blockly:', error);
}
}
};
// Try immediate resize
setTimeout(resizeBlockly, 50);
// Also try after longer delay (in case first one fails)
setTimeout(resizeBlockly, 200);
return true; // Blockly is now visible
} else {
// SWITCH TO TEXT EDITOR MODE
// Step 1: Show text editor
textEditorContainer.style.display = 'flex';
textEditorContainer.style.flex = '1';
// Step 2: Hide Blockly container
blocklyContainer.style.display = 'none';
return false; // Text editor is now visible
}
}
Button Label Update (in app.js):
// Blocks toggle button handler
const blocksToggleBtn = document.getElementById('blocks-toggle-button');
if (blocksToggleBtn) {
blocksToggleBtn.addEventListener('click', () => {
const integration = blocklyIntegration || window.blocklyIntegration;
if (integration) {
const isBlocklyVisible = integration.toggleVisibility();
// Update button text to reflect current mode
blocksToggleBtn.textContent = isBlocklyVisible ? 'Code' : 'Blocks';
} else {
// Fallback: Try to initialize if not ready
if (editor && typeof Blockly !== 'undefined') {
blocklyIntegration = new BlocklyIntegration(editor, interpreter, shapeMgr);
window.blocklyIntegration = blocklyIntegration;
const isBlocklyVisible = blocklyIntegration.toggleVisibility();
blocksToggleBtn.textContent = isBlocklyVisible ? 'Code' : 'Blocks';
}
}
});
}
HTML Structure Change:
The HTML was updated to have separate containers:
<!-- BEFORE (single container, didn't support proper toggling) -->
<div class="editor-container">
<textarea id="code-editor"></textarea>
<div id="blocklyDiv" style="display: none;"></div>
</div>
<!-- AFTER (separate containers for clean switching) -->
<div class="editor-container" id="text-editor-container">
<textarea id="code-editor"></textarea>
</div>
<div class="editor-container" id="blockly-editor-container" style="display: none;">
<div id="blocklyDiv"></div>
</div>
Why This Was Necessary:
- Container Swapping: Only one editor should be visible at a time. Having separate containers makes this explicit and prevents layout conflicts.
- Blockly Resize: Blockly needs explicit resize calls when its container becomes visible. The
Blockly.svgResize()method recalculates all SVG dimensions and workspace layout. - Multiple Resize Attempts: Using multiple
setTimeoutcalls ensures resize happens after the browser has completed layout calculations, which can take multiple render cycles. - Button Label: Users need visual feedback about which mode they're in and what clicking will do.
What Happened Without This: Blockly would appear but blocks would be positioned incorrectly or overlapping, the workspace wouldn't respond to mouse interactions properly, and users couldn't tell which mode was active.
Bidirectional Sync with Visibility Checks
Issue: The initial sync implementation would sync regardless of which editor was visible. This caused several problems:
- Unnecessary sync operations when target editor was hidden
- Sync conflicts when switching between editors
- Code execution triggered unnecessarily
- Potential race conditions
What Was Added: Visibility checks in both sync directions to only sync when the target editor is actually visible.
Specific Implementation - Blocks → Text Sync:
setupSync() {
if (!this.workspace || !this.editor) {
// Retry if workspace not ready yet
if (!this.workspaceReady) {
setTimeout(() => this.setupSync(), 100);
}
return;
}
// SYNC FROM BLOCKS TO TEXT EDITOR
this.workspace.addChangeListener((event) => {
// Standard sync flag checks
if (this.syncingFromEditor) return;
if (event.type === Blockly.Events.UI) return;
// NEW: Check if text editor is actually visible
// Only sync if user can see the text editor (Blockly is hidden)
const textEditorContainer = document.getElementById('text-editor-container');
if (textEditorContainer && textEditorContainer.style.display === 'none') {
// Blockly is currently visible, user is working with blocks
// Don't sync to text editor because they can't see it
return;
}
try {
// Generate code from blocks
const code = Blockly.JavaScript.workspaceToCode(this.workspace);
// Only update if code actually changed
if (this.editor && this.editor.getValue() !== code) {
this.syncingFromBlocks = true;
this.editor.setValue(code);
// Trigger code execution to update canvas
if (window.runCode) {
setTimeout(() => {
window.runCode();
}, 200);
}
setTimeout(() => {
this.syncingFromBlocks = false;
}, 300);
}
} catch (error) {
console.error('Error syncing from blocks:', error);
}
});
// SYNC FROM TEXT EDITOR TO BLOCKS
if (this.editor && this.editor.on) {
this.editor.on('change', () => {
// Standard sync flag checks
if (this.syncingFromBlocks) return;
// NEW: Check if Blockly is actually visible
// Only sync if user can see Blockly (text editor is hidden)
const blocklyContainer = document.getElementById('blockly-editor-container');
if (!blocklyContainer || blocklyContainer.style.display === 'none') {
// Text editor is currently visible, user is working with text
// Don't sync to blocks because they can't see Blockly
return;
}
try {
const code = this.editor.getValue();
this.updateBlocksFromCode(code);
} catch (error) {
console.error('Error syncing from editor:', error);
}
});
}
}
Why This Was Necessary:
- Performance: Syncing to a hidden editor wastes CPU cycles on operations the user can't see
- User Experience: If user switches to text mode, they expect to see their text code, not code that was just synced from blocks while they weren't looking
- Sync Conflicts: Syncing while switching editors can cause timing issues where the wrong code overwrites the visible editor
- Code Execution: When syncing from blocks to text, we trigger
runCode()to update the canvas. If the user isn't looking at text, this execution is unnecessary and can cause confusion
What Happened Without This:
- Code would update in hidden editors, causing confusion when switching modes
- Unnecessary code execution would occur
- Sync operations would compete with user input
- Potential for sync loops when rapidly switching modes
Renderer Selection Fallback
Issue: The initial implementation hardcoded renderer: 'thrasos', but different Blockly versions or CDN sources may not include all renderers. If a renderer doesn't exist, Blockly throws an error and fails to initialize.
What Was Added: Renderer availability checking with fallback to default renderer.
Specific Implementation:
setupWorkspace() {
// ... find blocklyDiv and setup container ...
// Build base options
const injectOptions = {
toolbox: toolbox,
grid: { spacing: 20, length: 3, colour: '#ccc', snap: true },
zoom: { controls: true, wheel: true },
trashcan: true
// Note: renderer not set yet - we'll add it conditionally
};
// NEW: Check if preferred renderer is available
// Different Blockly versions have different renderers
try {
// Check if renderers object exists and has the renderer we want
if (Blockly.renderers && typeof Blockly.renderers === 'object') {
// Check if 'thrasos' renderer is available
if (Blockly.renderers['thrasos']) {
injectOptions.renderer = 'thrasos';
console.log('Using thrasos renderer');
} else {
console.warn('thrasos renderer not available, using default');
}
} else {
// Blockly.renderers doesn't exist in this version
console.warn('Blockly.renderers not available, using default renderer');
}
} catch (e) {
// If anything goes wrong, just use default renderer
console.warn('Error checking renderer availability:', e);
// Don't set renderer option - Blockly will use default
}
// Initialize workspace with options (with or without explicit renderer)
this.workspace = Blockly.inject(blocklyDiv, injectOptions);
}
Why This Was Necessary:
- Version Compatibility: Different Blockly versions include different renderers. CDN versions may differ from npm versions
- Graceful Degradation: If the preferred renderer isn't available, the app should still work with the default renderer
- Error Prevention: Without this check, Blockly throws "Renderer 'thrasos' not found" error, preventing initialization
- Future-Proofing: As Blockly updates, renderer availability may change. This code adapts automatically
What Happened Without This: If thrasos renderer wasn't available (depending on Blockly version or CDN), Blockly would throw an error during inject() and the workspace would never initialize. Users would see a blank Blockly area with JavaScript errors in the console.
Alternative Renderers: Other renderer options include:
'geras'- Classic Blockly renderer (usually always available)'zelos'- Alternative modern renderer- Default renderer (if no renderer specified, Blockly uses its default)
The code could be extended to try multiple renderers in order of preference.