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: shape comes first, then the type circle, then the name c1, 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 circle as circl → 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:

  1. Drag-and-drop interface - Users drag blocks from a toolbox
  2. Code generation - Convert blocks to text code automatically
  3. Block creation from code - Convert text code back to blocks
  4. Customizable blocks - Define your own block types (circle, rectangle, etc.)
  5. 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:

  1. Core library first (blockly_compressed.js)
  2. Then blocks (blocks_compressed.js) - needs core to exist
  3. Then generator (javascript_compressed.js) - needs blocks to exist
  4. 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:
    1. Script tags in wrong order: Your code runs before Blockly loads
    2. Script tags missing: Forgot to add the <script> tags
    3. CDN URL wrong: Typo in URL, file doesn't exist
    4. 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:
    1. Typo in URL: blockly_compressed.js misspelled
    2. Wrong version: URL points to version that doesn't exist
    3. CDN path changed: unpkg.com path structure changed
  • 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:
    1. Missing script: One of the 4 scripts not loaded
    2. Wrong load order: Scripts loaded in wrong order (need core first)
    3. 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:
    1. msg/en.js not included: Missing the language script tag
    2. Wrong language file: Loaded msg/es.js instead of msg/en.js (or vice versa)
    3. 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:

  1. Open index.html
  2. Add script tag for blockly_compressed.js (core library)
  3. Add script tag for blocks_compressed.js (standard blocks)
  4. Add script tag for javascript_compressed.js (code generator)
  5. Add script tag for msg/en.js (localization)
  6. Load these before your application code
  7. 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 apart
  • length: 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 UI
  • wheel: 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:
    1. Blockly scripts not loaded: Missing script tags in HTML
    2. Scripts loaded after this code runs: Load order wrong
    3. Wrong Blockly version: Using old version that doesn't have inject method
  • 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:
    1. Div not in HTML: Missing <div id="blocklyDiv"> in HTML
    2. Wrong ID: Div has different ID like blockly-div or blocksDiv
    3. Script runs before HTML loads: DOM not ready yet
  • Fix: Add div to HTML: <div id="blocklyDiv" style="height:600px;"></div>, verify ID matches exactly (case-sensitive), use DOMContentLoaded event 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:
    1. Variable not defined: Forgot to create TOOLBOX_XML
    2. Wrong variable name: toolboxXml vs TOOLBOX_XML (case mismatch)
    3. 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:
    1. Empty toolbox: contents: [] means no blocks available
    2. Blocks not registered: Custom blocks defined but not registered with Blockly
    3. Toolbox XML invalid: XML syntax error in toolbox definition
  • 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:
    1. Renderer not supported: Using renderer name that doesn't exist
    2. Block definitions incomplete: Missing color/style properties in block definitions
    3. 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:

  1. 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.

  2. grid - The visual grid is optional but helpful. It provides visual alignment guides and makes the workspace look more professional. The snap: true option is particularly useful - it automatically aligns blocks to grid positions, making the workspace neater.

  3. 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.

  4. 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:

  1. Reads the message0 string (e.g., 'shape circle %1 { radius: %2 }')
  2. Finds placeholders (%1, %2, etc.)
  3. Looks in args0 array to find what each placeholder represents
  4. Replaces placeholders with the actual input fields/blocks
  5. 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:

  1. Blockly reads message0: 'shape circle %1 { radius: %2 }'
  2. Finds %1 placeholder
  3. Looks at args0[0]{ type: 'field_input', name: 'NAME', text: 'c1' }
  4. Creates a text input field with default value "c1"
  5. Finds %2 placeholder
  6. Looks at args0[1]{ type: 'field_number', name: 'RADIUS', value: 50 }
  7. 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:

  • %1 corresponds to args0[0] (first element, index 0)
  • %2 corresponds to args0[1] (second element, index 1)
  • %3 corresponds to args0[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 value
  • text: 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 identifier
  • value: Default numeric value

Dropdown (field_dropdown):

{ type: 'field_dropdown', name: 'SHAPE_TYPE', options: [['circle', 'CIRCLE'], ['rectangle', 'RECT']] }
  • Creates a dropdown menu
  • name: Internal identifier
  • options: 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 block
  • message0/1: Text templates that define what the block says
  • args0/1: Define what the placeholders (%1, %2) represent
  • previousStatement/nextStatement: Allow blocks to stack vertically
  • colour: 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:

  1. Create blocks-umd.js file (UMD format for script tag loading)
  2. Call Blockly.defineBlocksWithJsonArray() with array of block definitions
  3. Define block type with unique identifier
  4. Set message0 with text template and placeholders (%1, %2)
  5. Define args0 array with input field definitions
  6. Set message1 for additional block lines
  7. Define args1 array with statement input
  8. Set previousStatement and nextStatement to null (allow stacking)
  9. Set colour for visual category
  10. Register code generator function as Blockly.JavaScript['block_type']
  11. Get field values using getFieldValue()
  12. Collect code from connected blocks using helper function
  13. Return formatted AQUI code string
  14. This creates a custom block that generates AQUI code

What's happening:

  • message0 is the first line of the block. %1 is a placeholder for the first input (the name field)
  • args0 defines that input - it's a text field where you type the shape name
  • message1 and args1 add a second section where you can attach property blocks
  • previousStatement and nextStatement mean 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: 50color: redfill: 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:

  1. Create function collectLinesUnique_() with block and input parameters
  2. Initialize empty lines array
  3. Get first connected block using getInputTargetBlock()
  4. Add while loop that continues while child exists
  5. Get code generator function for child block type
  6. Check if generator is a function
  7. If yes, call generator with child block and trim result
  8. If code generated, add to lines array with 2-space indentation
  9. Move to next block using getNextBlock()
  10. After loop, join all lines with newlines
  11. Return joined string
  12. 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:

  1. Filter out unnecessary events (UI events, field changes during rebuild)
  2. Generate code from blocks
  3. 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 syncingFromBlocks is 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:

  1. Call blocklyWorkspace.addChangeListener() with event handler
  2. Check if event is UI-only, return early if so
  3. Check if event is field change, return early if so
  4. Check syncingFromBlocks flag, return early if set
  5. Wrap in try-catch for error handling
  6. Generate code using Blockly.JavaScript.workspaceToCode()
  7. Compare generated code with current editor content
  8. If different, set syncingFromBlocks flag to true
  9. Use editor.operation() to wrap editor updates
  10. Call editor.setValue() with generated code
  11. Call runCode() to execute and update canvas
  12. In catch block, log warning for errors
  13. In finally block, clear syncingFromBlocks flag
  14. This listener syncs blocks → text while preventing loops

How it works:

  1. Blockly fires a change event
  2. We check if it's a real change (not just UI stuff)
  3. We check the syncingFromBlocks flag - if we're already syncing, ignore it
  4. Generate code from blocks using Blockly.JavaScript.workspaceToCode()
  5. If it's different from what's in the editor, update the editor
  6. Run the code to update the canvas
  7. 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 outputConnection and input.connection. Blockly handles the visual wiring.
  • You need to call blk.initSvg() and blk.render() after creating blocks, or they won't appear.

Preventing Sync Loops

The syncingFromBlocks flag is crucial. Without it:

  1. User changes blocks
  2. Blocks → Text sync fires
  3. Text editor updates
  4. Text → Blocks sync fires (if enabled)
  5. Blocks update
  6. Blocks → Text sync fires again
  7. Infinite loop

We prevent this by:

  • Setting syncingFromBlocks = true before 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:

  1. Finds the top-level blocks (blocks not connected to anything above)
  2. For each block, calls Blockly.JavaScript[block.type](block)
  3. 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.js loads 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 syncingFromBlocks flag
  • 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 stmtToBlock handles all statement types
  • Make sure exprToBlock handles 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:

  1. 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.
  2. Create block instances: When users drag blocks from the toolbox, Blockly needs the block definition to know how to render them.
  3. 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', ...}]):

  1. Blockly parses the JSON array
  2. For each block definition, it:
    • Stores the block structure (appearance, inputs, etc.) in an internal registry
    • Creates a mapping: blockTypeblockDefinition
  3. 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:

  1. Looks up each block's code generator function using its type
  2. Calls that function to generate code
  3. 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:

  1. Both editors could be visible simultaneously
  2. Blockly wouldn't resize when shown
  3. Button label didn't reflect current state
  4. 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:

  1. Container Swapping: Only one editor should be visible at a time. Having separate containers makes this explicit and prevents layout conflicts.
  2. Blockly Resize: Blockly needs explicit resize calls when its container becomes visible. The Blockly.svgResize() method recalculates all SVG dimensions and workspace layout.
  3. Multiple Resize Attempts: Using multiple setTimeout calls ensures resize happens after the browser has completed layout calculations, which can take multiple render cycles.
  4. 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:

  1. Unnecessary sync operations when target editor was hidden
  2. Sync conflicts when switching between editors
  3. Code execution triggered unnecessarily
  4. 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:

  1. Performance: Syncing to a hidden editor wastes CPU cycles on operations the user can't see
  2. 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
  3. Sync Conflicts: Syncing while switching editors can cause timing issues where the wrong code overwrites the visible editor
  4. 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:

  1. Version Compatibility: Different Blockly versions include different renderers. CDN versions may differ from npm versions
  2. Graceful Degradation: If the preferred renderer isn't available, the app should still work with the default renderer
  3. Error Prevention: Without this check, Blockly throws "Renderer 'thrasos' not found" error, preventing initialization
  4. 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.

results matching ""

    No results matching ""