Deep Understanding Google Blockly - Part 1: Architecture & Blocks

Explore the intricate architecture and block system of Google Blockly in this in-depth technical analysis, covering its layered design, injection process, and core components.

Deep Understanding Google Blockly - Part 1: Architecture & Blocks

A comprehensive technical deep-dive into Blockly's core architecture and block system

Part 1 of 4


Table of Contents

  1. Introduction
  2. Core Architecture Overview
  3. The Injection Process
  4. The Block System
  5. Block Lifecycle
  6. Block Definition System
  7. Input System

Introduction

Google Blockly is a sophisticated visual programming library that transforms programming into an intuitive drag-and-drop interface. While deceptively simple from a user's perspective, the underlying architecture is a masterclass in software engineering.

This 4-part series provides a comprehensive technical analysis of Blockly v12.3.1, exploring:

  • Part 1 (this): Core architecture and block system
  • Part 2: SVG rendering pipeline and visual representation
  • Part 3: Code generation and language translation
  • Part 4: Advanced topics (events, serialization, plugins)

What Makes Blockly Unique?

Unlike simple drag-and-drop libraries, Blockly implements:

  • Type-safe connections with compatibility checking
  • Multi-renderer architecture (Geras, Thrasos, Zelos)
  • Language-agnostic code generation (JavaScript, Python, Dart, Lua, PHP)
  • Real-time SVG rendering with sophisticated path generation
  • Undo/redo system with event replay
  • Serialization/deserialization to JSON and XML

Core Architecture Overview

Architectural Layers

Blockly follows a layered architecture with clear separation of concerns:

┌─────────────────────────────────────────────────┐
│ User Interaction Layer                          │
│ (Gesture, Mouse/Touch Events, Drag)             │
└─────────────────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────┐
│ Workspace SVG Layer                             │
│ (WorkspaceSvg, Toolbox, Trashcan, Flyout)       │
└─────────────────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────┐
│ Block Layer                                     │
│ (Block, BlockSvg, Connections)                  │
└─────────────────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────┐
│ Rendering System                                │
│ (Renderer, RenderInfo, Drawer, PathObject)      │
└─────────────────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────┐
│ SVG DOM Layer                                   │
│ (SVG Elements, Paths)                           │
└─────────────────────────────────────────────────┘

Directory Structure

The source code is organized into focused modules:

blockly/
├── core/                  # Core Blockly functionality
│   ├── blockly.ts         # Main entry point, API exports
│   ├── block.ts           # Base Block class (headless)
│   ├── block_svg.ts       # SVG-rendered Block class
│   ├── workspace.ts       # Base Workspace class
│   ├── workspace_svg.ts   # SVG Workspace implementation
│   ├── inject.ts          # Initialization & DOM injection
│   ├── generator.ts       # Code generation base class
│   ├── connection.ts      # Block connection logic
│   ├── field.ts           # Field base class
│   ├── renderers/         # Rendering engines
│   │   ├── common/        # Shared rendering components
│   │   ├── geras/         # Geras renderer (default)
│   │   ├── thrasos/       # Thrasos renderer
│   │   └── zelos/         # Zelos renderer (Scratch-style)
│   ├── events/            # Event system
│   ├── serialization/     # JSON/XML serialization
│   └── inputs/            # Input types
├── blocks/                # Standard block definitions
│   ├── logic.ts           # Boolean/conditional blocks
│   ├── loops.ts           # Loop blocks
│   ├── math.ts            # Math operation blocks
│   └── ...
└── generators/            # Code generators
    ├── javascript/        # JavaScript generator
    ├── python/            # Python generator
    ├── dart/              # Dart generator
    ├── lua/               # Lua generator
    └── php/               # PHP generator

Key Design Patterns

  1. Separation of Concerns
    • Block (headless logic) vs BlockSvg (visual representation)
    • Workspace (data) vs WorkspaceSvg (rendering)
    • Connection (logic) vs RenderedConnection (visual)
  2. Strategy Pattern
    • Multiple renderers (Geras, Thrasos, Zelos)
    • Pluggable code generators
    • Customizable connection checkers
  3. Observer Pattern
    • Event system for block changes
    • Workspace change listeners
    • Field value observers
  4. Factory Pattern
    • Block creation via prototypes
    • Field registration and instantiation

The Injection Process

How Blockly Initializes

The entry point is the inject() function in core/inject.ts:

export function inject(
  container: Element | string,
  opt_options?: BlocklyOptions,
): WorkspaceSvg {
  // 1. Resolve container element
  let containerElement: Element | null = null;
  if (typeof container === 'string') {
    containerElement =
      document.getElementById(container) || document.querySelector(container);
  } else {
    containerElement = container;
  }

  // 2. Create options object
  const options = new Options(opt_options || ({} as BlocklyOptions));

  // 3. Create injection div
  const subContainer = document.createElement('div');
  dom.addClass(subContainer, 'injectionDiv');
  containerElement!.appendChild(subContainer);

  // 4. Create SVG DOM structure
  const svg = createDom(subContainer, options);

  // 5. Create main workspace
  const workspace = createMainWorkspace(subContainer, svg, options);

  // 6. Initialize workspace
  init(workspace);
  return workspace;
}

DOM Creation Process

The createDom() function builds the SVG structure:

function createDom(container: HTMLElement, options: Options): SVGElement {
  // Force LTR layout (Blockly handles RTL internally)
  container.setAttribute('dir', 'LTR');

  // Inject CSS
  Css.inject(options.hasCss, options.pathToMedia);

  // Create SVG element
  const svg = dom.createSvgElement(
    Svg.SVG,
    {
      'xmlns': dom.SVG_NS,
      'xmlns:html': dom.HTML_NS,
      'xmlns:xlink': dom.XLINK_NS,
      'version': '1.1',
      'class': 'blocklySvg',
    },
    container,
  );

  // Create defs for patterns/filters
  const defs = dom.createSvgElement(Svg.DEFS, {}, svg);

  // Create grid pattern
  const rnd = String(Math.random()).substring(2);
  options.gridPattern = Grid.createDom(rnd, options.gridOptions, defs, container);
  return svg;
}

Workspace Initialization

The createMainWorkspace() function sets up the workspace:

function createMainWorkspace(
  injectionDiv: HTMLElement,
  svg: SVGElement,
  options: Options,
): WorkspaceSvg {
  // Create workspace
  const mainWorkspace = new WorkspaceSvg(options);
  mainWorkspace.scale = options.zoomOptions.startScale;

  // Create workspace DOM
  svg.appendChild(
    mainWorkspace.createDom('blocklyMainBackground', injectionDiv)
  );

  // Apply renderer and theme classes
  const rendererClassName = mainWorkspace.getRenderer().getClassName();
  dom.addClass(injectionDiv, rendererClassName);
  const themeClassName = mainWorkspace.getTheme().getClassName();
  dom.addClass(injectionDiv, themeClassName);

  // Add flyout (if no categories)
  if (!options.hasCategories && options.languageTree) {
    const flyout = mainWorkspace.addFlyout(Svg.SVG);
    dom.insertAfter(flyout, svg);
  }

  // Add workspace components
  if (options.hasTrashcan) {
    mainWorkspace.addTrashcan();
  }
  if (options.zoomOptions?.controls) {
    mainWorkspace.addZoomControls();
  }

  return mainWorkspace;
}

The Block System

Block Class Hierarchy

Blockly uses inheritance to separate concerns:

┌─────────────┐
│    Block    │ (Headless base class)
└─────────────┘
       ↑
       │ extends
       │
┌─────────────┐
│  BlockSvg   │ (SVG rendering + interaction)
└─────────────┘

The Base Block Class

From core/block.ts:

export class Block {
  // Identity
  id: string;
  type: string;
  workspace: Workspace;

  // Structure
  outputConnection: Connection | null = null;
  nextConnection: Connection | null = null;
  previousConnection: Connection | null = null;
  inputList: Input[] = [];

  // Parent-child relationships
  protected parentBlock_: this | null = null;
  protected childBlocks_: this[] = [];

  // Visual properties
  private colour_ = '#000000';
  private hue: number | null = null;
  protected styleName_ = '';

  // State
  private deletable = true;
  private movable = true;
  private editable = true;
  private shadow = false;
  protected collapsed_ = false;

  // Fields and icons
  icons: IIcon[] = [];

  // Position
  private readonly xy: Coordinate;

  constructor(workspace: Workspace, prototypeName: string, opt_id?: string) {
    this.workspace = workspace;
    this.id =
      opt_id && !workspace.getBlockById(opt_id) ? opt_id : idGenerator.genUid();
    workspace.setBlockById(this.id, this);
    this.xy = new Coordinate(0, 0);

    // Copy prototype properties
    if (prototypeName) {
      this.type = prototypeName;
      const prototype = Blocks[prototypeName];
      if (!prototype || typeof prototype !== 'object') {
        throw TypeError('Invalid block definition for type: ' + prototypeName);
      }
      Object.assign(this, prototype);
    }
    workspace.addTopBlock(this);
    workspace.addTypedBlock(this);

    // Initialize if this is the final class
    if (new.target === Block) {
      this.doInit_();
    }
  }

  protected doInit_() {
    const existingGroup = eventUtils.getGroup();
    if (!existingGroup) {
      eventUtils.setGroup(true);
    }
    try {
      // Call block's init function
      if (this.init) {
        this.init();
      }
      this.initialized = true;
    } finally {
      if (!existingGroup) {
        eventUtils.setGroup(false);
      }
    }
  }
}

The BlockSvg Class

From core/block_svg.ts:

export class BlockSvg
  extends Block
  implements
    IBoundedElement,
    IContextMenu,
    ICopyable<BlockCopyData>,
    IDraggable,
    IDeletable,
    IFocusableNode
{
  // SVG elements
  private svgGroup: SVGGElement;
  pathObject: IPathObject;

  // Dimensions
  height = 0;
  width = 0;
  childlessWidth = 0;

  // Visual state
  private visuallyDisabled = false;
  private dragging = false;

  // Rendering
  style: BlockStyle;
  override workspace: WorkspaceSvg;

  // Connections (typed as RenderedConnection)
  override outputConnection!: RenderedConnection;
  override nextConnection!: RenderedConnection;
  override previousConnection!: RenderedConnection;

  constructor(workspace: WorkspaceSvg, prototypeName: string, opt_id?: string) {
    super(workspace, prototypeName, opt_id);
    if (!workspace.rendered) {
      throw TypeError('Cannot create rendered block in headless workspace');
    }

    // Create SVG group
    this.svgGroup = dom.createSvgElement(Svg.G, {});
    if (prototypeName) {
      dom.addClass(this.svgGroup, prototypeName);
    }

    // Get block style from renderer
    this.style = workspace.getRenderer()
      .getConstants()
      .getBlockStyle(null);

    // Create path object for rendering
    this.pathObject = workspace.getRenderer()
      .makePathObject(this.svgGroup, this.style);

    // Setup tooltip
    const svgPath = this.pathObject.svgPath;
    (svgPath as any).tooltip = this;
    Tooltip.bindMouseEvents(svgPath);

    // Set data-id attribute
    this.svgGroup.setAttribute('data-id', this.id);
    this.doInit_();
  }

  initSvg() {
    if (this.initialized) return;

    // Initialize inputs
    for (const input of this.inputList) {
      input.init();
    }

    // Initialize icons
    for (const icon of this.getIcons()) {
      icon.initView(this.createIconPointerDownListener(icon));
      icon.updateEditable();
    }

    // Apply colors
    this.applyColour();

    // Update movability
    this.pathObject.updateMovable(this.isMovable() || this.isInFlyout);

    // Bind mouse events
    const svg = this.getSvgRoot();
    if (svg) {
      browserEvents.conditionalBind(svg, 'pointerdown', this, this.onMouseDown);
    }

    // Append to canvas
    if (!svg.parentNode) {
      this.workspace.getCanvas().appendChild(svg);
    }
    this.initialized = true;
  }
}

Block Lifecycle

Creation Flow

1. new BlockSvg(workspace, 'math_number')
   │
   ├─> super(workspace, 'math_number') [Block constructor]
   │   ├─> Generate/assign ID
   │   ├─> Register with workspace
   │   └─> Copy prototype properties (from Blocks['math_number'])
   │
   ├─> Create SVG group element
   ├─> Get renderer style
   └─> Create PathObject

2. doInit_()
   │
   └─> Call this.init() [from block definition]
       ├─> appendField()
       ├─> appendValueInput()
       ├─> setOutput()
       └─> setColour()

3. initSvg()
   │
   ├─> Initialize all inputs
   ├─> Initialize all icons
   ├─> Apply colours
   ├─> Bind event handlers
   └─> Append to workspace canvas

4. render() [Triggered later]
   │
   └─> Renderer.render(block)
       ├─> RenderInfo.measure()
       └─> Drawer.draw()

Disposal Flow

1. dispose(healStack?: boolean)
   │
   ├─> Fire BlockDelete event
   ├─> Unplug from parent
   ├─> Dispose all child blocks
   │   └─> Recursively call dispose()
   │
   ├─> Dispose all inputs
   │   └─> Disconnect connections
   │
   ├─> Dispose all icons
   ├─> Remove from workspace
   ├─> Remove SVG elements
   └─> Mark as disposed

Block Definition System

JSON Block Definitions

Blocks are defined using JSON, typically in the /blocks/ directory:

// From blocks/logic.ts
export const blocks = createBlockDefinitionsFromJsonArray([
  {
    'type': 'logic_boolean',
    'message0': '%1',
    'args0': [
      {
        'type': 'field_dropdown',
        'name': 'BOOL',
        'options': [
          ['%{BKY_LOGIC_BOOLEAN_TRUE}', 'TRUE'],
          ['%{BKY_LOGIC_BOOLEAN_FALSE}', 'FALSE'],
        ],
      },
    ],
    'output': 'Boolean',
    'style': 'logic_blocks',
    'tooltip': '%{BKY_LOGIC_BOOLEAN_TOOLTIP}',
    'helpUrl': '%{BKY_LOGIC_BOOLEAN_HELPURL}',
  },
]);

Block Definition Structure

interface BlockDefinition {
  type: string; // Unique block type identifier
  message0?: string; // First message line (with %1, %2 placeholders)
  message1?: string; // Second message line
  args0?: FieldDef[]; // Arguments for message0
  args1?: FieldDef[]; // Arguments for message1
  output?: string | string[]; // Output connection type(s)
  previousStatement?: string | string[] | null;
  nextStatement?: string | string[] | null;
  colour?: number; // HSV hue (0-360)
  style?: string; // Style name from theme
  tooltip?: string;
  helpUrl?: string;
  inputsInline?: boolean;
  mutator?: string; // Mutator extension name
  extensions?: string[]; // Extension names
}

Initialization Function

Blocks can define an init() function for programmatic setup:

Blocks['custom_block'] = {
  init: function() {
    this.appendDummyInput()
        .appendField('custom block');
    this.appendValueInput('VALUE')
        .setCheck('Number')
        .appendField('value');
    this.appendStatementInput('DO')
        .appendField('do');
    this.setOutput(true, 'String');
    this.setColour(230);
    this.setTooltip('A custom block');
    this.setHelpUrl('http://example.com');
  }
};

Input System

Input Types

Blockly supports four input types:

1. Value Input - Accepts blocks with output connections

this.appendValueInput('NUM')
    .setCheck('Number')
    .appendField('value:');

2. Statement Input - Accepts stack blocks

this.appendStatementInput('DO')
    .appendField('repeat');

3. Dummy Input - No connection, just fields

this.appendDummyInput()
    .appendField('hello')
    .appendField(new FieldTextInput('world'), 'TEXT');

4. End Row Input - Forces a line break

this.appendEndRowInput();

Input Class Structure

From core/inputs/input.ts:

export class Input {
  type: number;
  name: string;
  connection: Connection | null = null;
  fieldRow: Field[] = [];

  constructor(type: number, name: string, block: Block) {
    this.type = type;
    this.name = name;
    this.sourceBlock_ = block;

    // Create connection if needed
    if (type === inputTypes.VALUE || type === inputTypes.STATEMENT) {
      this.connection = this.makeConnection_();
    }
  }

  appendField(field: string | Field, opt_name?: string): Input {
    // Convert string to FieldLabel
    if (typeof field === 'string') {
      field = new FieldLabel(field);
    }
    // Set field name
    if (opt_name) {
      field.name = opt_name;
    }
    // Initialize if block is already initialized
    if (this.sourceBlock_.rendered) {
      field.init();
    }
    // Set source block
    field.setSourceBlock(this.sourceBlock_);
    // Add to field row
    this.fieldRow.push(field);
    return this;
  }

  setCheck(check: string | string[] | null): Input {
    if (this.connection) {
      this.connection.setCheck(check);
    }
    return this;
  }

  setAlign(align: Align): Input {
    this.align = align;
    return this;
  }
}

Input Alignment

enum Align {
  LEFT = -1,
  CENTRE = 0,
  RIGHT = 1,
}

// Usage
this.appendValueInput('A')
    .setAlign(Blockly.inputs.Align.RIGHT);

Summary

In Part 1, we've explored:

  • Core Architecture - Layered design with clear separation of concerns
  • Injection Process - How Blockly initializes and creates the SVG workspace
  • Block System - Block class hierarchy and lifecycle
  • Block Definitions - JSON-based block configuration
  • Input System - Different input types and their usage

This article is based on analysis of Blockly v12.3.1 source code. For the latest information, refer to the official Blockly documentation.

Subscribe to Root Logic

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe