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.
A comprehensive technical deep-dive into Blockly's core architecture and block system
Part 1 of 4
Table of Contents
- Introduction
- Core Architecture Overview
- The Injection Process
- The Block System
- Block Lifecycle
- Block Definition System
- 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
- Separation of Concerns
Block(headless logic) vsBlockSvg(visual representation)Workspace(data) vsWorkspaceSvg(rendering)Connection(logic) vsRenderedConnection(visual)
- Strategy Pattern
- Multiple renderers (Geras, Thrasos, Zelos)
- Pluggable code generators
- Customizable connection checkers
- Observer Pattern
- Event system for block changes
- Workspace change listeners
- Field value observers
- 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.