diff --git a/packages/docs/docs/codelabs/validation-and-warnings/codelab-overview.mdx b/packages/docs/docs/codelabs/validation-and-warnings/codelab-overview.mdx
index f402ac3b412..b340af8c811 100644
--- a/packages/docs/docs/codelabs/validation-and-warnings/codelab-overview.mdx
+++ b/packages/docs/docs/codelabs/validation-and-warnings/codelab-overview.mdx
@@ -26,12 +26,14 @@ In this codelab, you'll create a new custom block type that generates a list of
className="codelabImage"
/>
-You can find the code for the [completed custom block](https://github.com/RaspberryPiFoundation/blockly/tree/main/packages/docs/docs/codelabs/validation-and-warnings/complete-code/index.js) on GitHub.
+You can find the code for the [completed custom block](https://github.com/RaspberryPiFoundation/blockly/tree/main/packages/docs/docs/codelabs/validation-and-warnings/complete-code) on GitHub.
### What you'll need
- A browser.
- A text editor.
-- Basic knowledge of JavaScript.
+- Basic knowledge of HTML, CSS, and JavaScript.
+- node.js and npm installed ([instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)).
+- Comfort using the command line/terminal.
- Basic understanding of the [Blockly toolbox](/guides/configure/toolboxes/toolbox).
- Basic understanding of [using JSON to define custom blocks](/guides/create-custom-blocks/define/structure-json).
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/README.md b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/README.md
new file mode 100644
index 00000000000..c771b459051
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/README.md
@@ -0,0 +1,14 @@
+# Completed code
+
+The finished code for the [block validation and warnings](../codelab-overview.mdx) codelab.
+
+## Run it
+
+To run the complete code, navigate to the `complete-code` folder in your terminal, then run:
+
+```bash
+npm install # downloads the libraries the app needs (one time)
+npm start # opens the app at http://localhost:...
+```
+
+New to npm? See [installing node & npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
\ No newline at end of file
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/index.html b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/index.html
deleted file mode 100644
index f6ca51f2a12..00000000000
--- a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/index.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
-
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/index.js b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/index.js
deleted file mode 100644
index cdd841fc2c5..00000000000
--- a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/index.js
+++ /dev/null
@@ -1,138 +0,0 @@
-// Use Blockly's custom block JSON API to define a new block type.
-Blockly.common.defineBlocksWithJsonArray([
- {
- type: 'list_range',
- message0: 'create list of numbers from %1 up to %2',
- args0: [
- {
- type: 'field_number',
- name: 'FIRST',
- value: 0,
- min: 0,
- precision: 2,
- },
- {
- type: 'field_number',
- name: 'LAST',
- value: 5,
- min: 0,
- precision: 1,
- },
- ],
- output: 'Array',
- style: 'list_blocks',
- extensions: ['list_range_validation'],
- },
-]);
-
-Blockly.Extensions.register('list_range_validation', function () {
- // Add custom validation.
- this.getField('LAST').setValidator(function (newValue) {
- // Force an odd number.
- return Math.round((newValue - 1) / 2) * 2 + 1;
- });
-
- // Validate the entire block whenever any part of it changes,
- // and display a warning if the block cannot be made valid.
- this.setOnChange(function (event) {
- const first = this.getFieldValue('FIRST');
- const last = this.getFieldValue('LAST');
- const valid = first < last;
- this.setWarningText(
- valid
- ? null
- : `The first number (${first}) must be smaller than the last number (${last}).`,
- );
-
- // Disable invalid blocks (unless it's in a toolbox flyout,
- // since you can't drag disabled blocks to your workspace).
- if (!this.isInFlyout) {
- const initialGroup = Blockly.Events.getGroup();
- // Make it so the move and the disable event get undone together.
- Blockly.Events.setGroup(event.group);
- this.setDisabledReason(!valid, 'Invalid range');
- Blockly.Events.setGroup(initialGroup);
- }
- });
-});
-
-// Define how to generate JavaScript from the custom block.
-javascript.javascriptGenerator.forBlock['list_range'] = function (block) {
- const first = this.getFieldValue('FIRST');
- const last = this.getFieldValue('LAST');
- const numbers = [];
- for (let i = first; i <= last; i++) {
- numbers.push(i);
- }
- const code = '[' + numbers.join(', ') + ']';
- return [code, javascript.Order.NONE];
-};
-
-// Define which blocks are available in the toolbox.
-const toolbox = {
- kind: 'categoryToolbox',
- contents: [
- {
- kind: 'category',
- name: 'Blocks',
- categorystyle: 'list_category',
- contents: [
- {
- kind: 'block',
- type: 'list_range',
- },
- {
- kind: 'block',
- type: 'controls_forEach',
- },
- {
- kind: 'block',
- type: 'math_on_list',
- },
- {
- kind: 'block',
- type: 'text_print',
- },
- {
- kind: 'block',
- type: 'controls_flow_statements',
- },
- ],
- },
- {
- kind: 'category',
- name: 'Variables',
- categorystyle: 'variable_category',
- custom: 'VARIABLE',
- },
- ],
-};
-
-let workspace = null;
-
-/**
- * Initialize a Blockly workspace, and add a change listener to update the display of generated code.
- *
- * Called from index.html when the page initially loads.
- */
-function start() {
- // Create main workspace.
- workspace = Blockly.inject('blocklyDiv', {
- toolbox: toolbox,
- });
-
- workspace.addChangeListener((event) => {
- const code = javascript.javascriptGenerator.workspaceToCode(workspace);
- document.getElementById('generatedCodeContainer').value = code;
- });
-}
-
-/**
- * Generate JavaScript code from the Blockly workspace, and execute it.
- *
- * Called from index.html when the execute button is clicked.
- */
-function executeCode() {
- const code = javascript.javascriptGenerator.workspaceToCode(workspace);
- eval(code);
-}
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/package.json b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/package.json
new file mode 100644
index 00000000000..da2278276cb
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "validation-and-warnings-codelab",
+ "version": "1.0.0",
+ "description": "A sample app using Blockly",
+ "main": "index.js",
+ "private": true,
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "build": "webpack --mode production",
+ "start": "webpack serve --open --mode development"
+ },
+ "keywords": [
+ "blockly"
+ ],
+ "author": "",
+ "license": "Apache-2.0",
+ "devDependencies": {
+ "css-loader": "^6.7.1",
+ "html-webpack-plugin": "^5.5.0",
+ "source-map-loader": "^4.0.1",
+ "style-loader": "^3.3.1",
+ "webpack": "^5.93.0",
+ "webpack-cli": "^5.1.4",
+ "webpack-dev-server": "^5.0.4"
+ },
+ "dependencies": {
+ "blockly": "^13.0.0"
+ }
+}
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/blocks/text.js b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/blocks/text.js
new file mode 100644
index 00000000000..65f11b06e6a
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/blocks/text.js
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright 2026 Raspberry Pi Foundation
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as Blockly from 'blockly/core';
+
+// Create a custom block called 'add_text' that adds
+// text to the output div on the sample app.
+// This is just an example and you should replace this with your
+// own custom blocks.
+const addText = {
+ type: 'add_text',
+ message0: 'Add text %1',
+ args0: [
+ {
+ type: 'input_value',
+ name: 'TEXT',
+ check: 'String',
+ },
+ ],
+ previousStatement: null,
+ nextStatement: null,
+ colour: 160,
+ tooltip: '',
+ helpUrl: '',
+};
+
+// A custom block that generates a list of numbers counting up from FIRST to
+// LAST. It validates itself using the 'list_range_validation' extension.
+const listRange = {
+ type: 'list_range',
+ message0: 'create list of numbers from %1 up to %2',
+ args0: [
+ {
+ type: 'field_number',
+ name: 'FIRST',
+ value: 0,
+ min: 0,
+ precision: 2,
+ },
+ {
+ type: 'field_number',
+ name: 'LAST',
+ value: 5,
+ min: 0,
+ precision: 1,
+ },
+ ],
+ output: 'Array',
+ style: 'list_blocks',
+ extensions: ['list_range_validation'],
+};
+
+// Create the block definitions for the JSON-only blocks.
+// This does not register their definitions with Blockly.
+// This file has no side effects!
+export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
+ addText,
+ listRange,
+]);
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/generators/javascript.js b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/generators/javascript.js
new file mode 100644
index 00000000000..69a03ec0d8b
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/generators/javascript.js
@@ -0,0 +1,41 @@
+/**
+ * @license
+ * Copyright 2026 Raspberry Pi Foundation
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Order } from 'blockly/javascript';
+
+// Export all the code generators for our custom blocks,
+// but don't register them with Blockly yet.
+// This file has no side effects!
+export const forBlock = Object.create(null);
+
+forBlock['add_text'] = function (block, generator) {
+ const text = generator.valueToCode(block, 'TEXT', Order.NONE) || "''";
+ const addText = generator.provideFunction_(
+ 'addText',
+ `function ${generator.FUNCTION_NAME_PLACEHOLDER_}(text) {
+
+ // Add text to the output area.
+ const outputDiv = document.getElementById('output');
+ const textEl = document.createElement('p');
+ textEl.innerText = text;
+ outputDiv.appendChild(textEl);
+}`,
+ );
+ // Generate the function call for this block.
+ const code = `${addText}(${text});\n`;
+ return code;
+};
+
+forBlock['list_range'] = function (block, generator) {
+ const first = block.getFieldValue('FIRST');
+ const last = block.getFieldValue('LAST');
+ const numbers = [];
+ for (let i = first; i <= last; i++) {
+ numbers.push(i);
+ }
+ const code = '[' + numbers.join(', ') + ']';
+ return [code, Order.NONE];
+};
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.css b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.css
new file mode 100644
index 00000000000..f282700701f
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.css
@@ -0,0 +1,40 @@
+body {
+ margin: 0;
+ max-width: 100vw;
+}
+
+pre,
+code {
+ overflow: auto;
+}
+
+#pageContainer {
+ display: flex;
+ width: 100%;
+ max-width: 100vw;
+ height: 100vh;
+}
+
+#blocklyDiv {
+ flex-basis: 100%;
+ height: 100%;
+ min-width: 600px;
+}
+
+#outputPane {
+ display: flex;
+ flex-direction: column;
+ width: 400px;
+ flex: 0 0 400px;
+ overflow: auto;
+ margin: 1rem;
+}
+
+#generatedCode {
+ height: 50%;
+ background-color: rgb(247, 240, 228);
+}
+
+#output {
+ height: 50%;
+}
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.html b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.html
new file mode 100644
index 00000000000..36d8eeacd99
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Blockly Sample App
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.js b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.js
new file mode 100644
index 00000000000..ee146973f22
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/index.js
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * Copyright 2026 Raspberry Pi Foundation
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as Blockly from 'blockly';
+import { blocks } from './blocks/text';
+import { forBlock } from './generators/javascript';
+import { javascriptGenerator } from 'blockly/javascript';
+import { save, load } from './serialization';
+import { toolbox } from './toolbox';
+import './index.css';
+
+// Register the 'list_range_validation' extension that the list_range block
+// uses. The extension function runs every time a list_range block is created,
+// with the block instance available as `this`.
+Blockly.Extensions.register('list_range_validation', function () {
+ // Force the LAST field to be an odd number.
+ this.getField('LAST').setValidator(function (newValue) {
+ return Math.round((newValue - 1) / 2) * 2 + 1;
+ });
+
+ // Validate the entire block whenever any part of it changes,
+ // and display a warning if the block cannot be made valid.
+ this.setOnChange(function (event) {
+ const first = this.getFieldValue('FIRST');
+ const last = this.getFieldValue('LAST');
+ const valid = first < last;
+ this.setWarningText(
+ valid
+ ? null
+ : `The first number (${first}) must be smaller than the last number (${last}).`,
+ );
+
+ // Disable invalid blocks (unless it's in a toolbox flyout,
+ // since you can't drag disabled blocks to your workspace).
+ if (!this.isInFlyout) {
+ const initialGroup = Blockly.Events.getGroup();
+ // Make it so the move and the disable event get undone together.
+ Blockly.Events.setGroup(event.group);
+ this.setDisabledReason(!valid, 'Invalid range');
+ Blockly.Events.setGroup(initialGroup);
+ }
+ });
+});
+
+// Register the blocks and generator with Blockly
+Blockly.common.defineBlocks(blocks);
+Object.assign(javascriptGenerator.forBlock, forBlock);
+
+// Set up UI elements and inject Blockly
+const codeDiv = document.getElementById('generatedCode').firstChild;
+const outputDiv = document.getElementById('output');
+const blocklyDiv = document.getElementById('blocklyDiv');
+const ws = Blockly.inject(blocklyDiv, { toolbox });
+
+// This function resets the code and output divs, shows the
+// generated code from the workspace, and evals the code.
+// In a real application, you probably shouldn't use `eval`.
+const runCode = () => {
+ const code = javascriptGenerator.workspaceToCode(ws);
+ codeDiv.innerText = code;
+
+ outputDiv.innerHTML = '';
+
+ // Wrap `eval` in a `try/catch` so that any runtime errors are
+ // logged to the console, instead of failing quietly.
+ try {
+ eval(code);
+ } catch (error) {
+ console.log(error);
+ }
+};
+
+// Load the initial state from storage and run the code.
+load(ws);
+runCode();
+
+// Every time the workspace changes state, save the changes to storage.
+ws.addChangeListener((e) => {
+ // UI events are things like scrolling, zooming, etc.
+ // No need to save after one of these.
+ if (e.isUiEvent) return;
+ save(ws);
+});
+
+// Whenever the workspace changes meaningfully, run the code again.
+ws.addChangeListener((e) => {
+ // Don't run the code when the workspace finishes loading; we're
+ // already running it once when the application starts.
+ // Don't run the code during drags; we might have invalid state.
+ if (
+ e.isUiEvent ||
+ e.type == Blockly.Events.FINISHED_LOADING ||
+ ws.isDragging()
+ ) {
+ return;
+ }
+ runCode();
+});
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/serialization.js b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/serialization.js
new file mode 100644
index 00000000000..e3e50bd08c9
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/serialization.js
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright 2026 Raspberry Pi Foundation
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as Blockly from 'blockly/core';
+
+// Use a unique storage key for this codelab
+const storageKey = 'validationWorkspace';
+
+/**
+ * Saves the state of the workspace to browser's local storage.
+ * @param {Blockly.Workspace} workspace Blockly workspace to save.
+ */
+export const save = function (workspace) {
+ const data = Blockly.serialization.workspaces.save(workspace);
+ window.localStorage?.setItem(storageKey, JSON.stringify(data));
+};
+
+/**
+ * Loads saved state from local storage into the given workspace.
+ * @param {Blockly.Workspace} workspace Blockly workspace to load into.
+ */
+export const load = function (workspace) {
+ const data = window.localStorage?.getItem(storageKey);
+ if (!data) return;
+
+ // Don't emit events during loading.
+ Blockly.Events.disable();
+ Blockly.serialization.workspaces.load(JSON.parse(data), workspace, false);
+ Blockly.Events.enable();
+};
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/toolbox.js b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/toolbox.js
new file mode 100644
index 00000000000..1ef5a0d4e52
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/src/toolbox.js
@@ -0,0 +1,633 @@
+/**
+ * @license
+ * Copyright 2026 Raspberry Pi Foundation
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+This toolbox contains nearly every single built-in block that Blockly offers,
+in addition to the custom block 'add_text' this sample app adds.
+You probably don't need every single block, and should consider either rewriting
+your toolbox from scratch, or carefully choosing whether you need each block
+listed here.
+*/
+
+export const toolbox = {
+ kind: 'categoryToolbox',
+ contents: [
+ {
+ kind: 'category',
+ name: 'Logic',
+ categorystyle: 'logic_category',
+ contents: [
+ {
+ kind: 'block',
+ type: 'controls_if',
+ },
+ {
+ kind: 'block',
+ type: 'logic_compare',
+ },
+ {
+ kind: 'block',
+ type: 'logic_operation',
+ },
+ {
+ kind: 'block',
+ type: 'logic_negate',
+ },
+ {
+ kind: 'block',
+ type: 'logic_boolean',
+ },
+ {
+ kind: 'block',
+ type: 'logic_null',
+ },
+ {
+ kind: 'block',
+ type: 'logic_ternary',
+ },
+ ],
+ },
+ {
+ kind: 'category',
+ name: 'Loops',
+ categorystyle: 'loop_category',
+ contents: [
+ {
+ kind: 'block',
+ type: 'controls_repeat_ext',
+ inputs: {
+ TIMES: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 10,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'controls_whileUntil',
+ },
+ {
+ kind: 'block',
+ type: 'controls_for',
+ inputs: {
+ FROM: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 1,
+ },
+ },
+ },
+ TO: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 10,
+ },
+ },
+ },
+ BY: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 1,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'controls_forEach',
+ },
+ {
+ kind: 'block',
+ type: 'controls_flow_statements',
+ },
+ ],
+ },
+ {
+ kind: 'category',
+ name: 'Math',
+ categorystyle: 'math_category',
+ contents: [
+ {
+ kind: 'block',
+ type: 'math_number',
+ fields: {
+ NUM: 123,
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_arithmetic',
+ inputs: {
+ A: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 1,
+ },
+ },
+ },
+ B: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 1,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_single',
+ inputs: {
+ NUM: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 9,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_trig',
+ inputs: {
+ NUM: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 45,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_constant',
+ },
+ {
+ kind: 'block',
+ type: 'math_number_property',
+ inputs: {
+ NUMBER_TO_CHECK: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 0,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_round',
+ fields: {
+ OP: 'ROUND',
+ },
+ inputs: {
+ NUM: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 3.1,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_on_list',
+ fields: {
+ OP: 'SUM',
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_modulo',
+ inputs: {
+ DIVIDEND: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 64,
+ },
+ },
+ },
+ DIVISOR: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 10,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_constrain',
+ inputs: {
+ VALUE: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 50,
+ },
+ },
+ },
+ LOW: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 1,
+ },
+ },
+ },
+ HIGH: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 100,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_random_int',
+ inputs: {
+ FROM: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 1,
+ },
+ },
+ },
+ TO: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 100,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'math_random_float',
+ },
+ {
+ kind: 'block',
+ type: 'math_atan2',
+ inputs: {
+ X: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 1,
+ },
+ },
+ },
+ Y: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 1,
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ {
+ kind: 'category',
+ name: 'Text',
+ categorystyle: 'text_category',
+ contents: [
+ {
+ kind: 'block',
+ type: 'text',
+ },
+ {
+ kind: 'block',
+ type: 'text_join',
+ },
+ {
+ kind: 'block',
+ type: 'text_append',
+ inputs: {
+ TEXT: {
+ shadow: {
+ type: 'text',
+ fields: {
+ TEXT: '',
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_length',
+ inputs: {
+ VALUE: {
+ shadow: {
+ type: 'text',
+ fields: {
+ TEXT: 'abc',
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_isEmpty',
+ inputs: {
+ VALUE: {
+ shadow: {
+ type: 'text',
+ fields: {
+ TEXT: '',
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_indexOf',
+ inputs: {
+ VALUE: {
+ block: {
+ type: 'variables_get',
+ },
+ },
+ FIND: {
+ shadow: {
+ type: 'text',
+ fields: {
+ TEXT: 'abc',
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_charAt',
+ inputs: {
+ VALUE: {
+ block: {
+ type: 'variables_get',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_getSubstring',
+ inputs: {
+ STRING: {
+ block: {
+ type: 'variables_get',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_changeCase',
+ inputs: {
+ TEXT: {
+ shadow: {
+ type: 'text',
+ fields: {
+ TEXT: 'abc',
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_trim',
+ inputs: {
+ TEXT: {
+ shadow: {
+ type: 'text',
+ fields: {
+ TEXT: 'abc',
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_count',
+ inputs: {
+ SUB: {
+ shadow: {
+ type: 'text',
+ },
+ },
+ TEXT: {
+ shadow: {
+ type: 'text',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_replace',
+ inputs: {
+ FROM: {
+ shadow: {
+ type: 'text',
+ },
+ },
+ TO: {
+ shadow: {
+ type: 'text',
+ },
+ },
+ TEXT: {
+ shadow: {
+ type: 'text',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'text_reverse',
+ inputs: {
+ TEXT: {
+ shadow: {
+ type: 'text',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'add_text',
+ inputs: {
+ TEXT: {
+ shadow: {
+ type: 'text',
+ fields: {
+ TEXT: 'abc',
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ {
+ kind: 'category',
+ name: 'Lists',
+ categorystyle: 'list_category',
+ contents: [
+ {
+ kind: 'block',
+ type: 'list_range',
+ },
+ {
+ kind: 'block',
+ type: 'lists_create_with',
+ },
+ {
+ kind: 'block',
+ type: 'lists_create_with',
+ },
+ {
+ kind: 'block',
+ type: 'lists_repeat',
+ inputs: {
+ NUM: {
+ shadow: {
+ type: 'math_number',
+ fields: {
+ NUM: 5,
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'lists_length',
+ },
+ {
+ kind: 'block',
+ type: 'lists_isEmpty',
+ },
+ {
+ kind: 'block',
+ type: 'lists_indexOf',
+ inputs: {
+ VALUE: {
+ block: {
+ type: 'variables_get',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'lists_getIndex',
+ inputs: {
+ VALUE: {
+ block: {
+ type: 'variables_get',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'lists_setIndex',
+ inputs: {
+ LIST: {
+ block: {
+ type: 'variables_get',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'lists_getSublist',
+ inputs: {
+ LIST: {
+ block: {
+ type: 'variables_get',
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'lists_split',
+ inputs: {
+ DELIM: {
+ shadow: {
+ type: 'text',
+ fields: {
+ TEXT: ',',
+ },
+ },
+ },
+ },
+ },
+ {
+ kind: 'block',
+ type: 'lists_sort',
+ },
+ {
+ kind: 'block',
+ type: 'lists_reverse',
+ },
+ ],
+ },
+ {
+ kind: 'sep',
+ },
+ {
+ kind: 'category',
+ name: 'Variables',
+ categorystyle: 'variable_category',
+ custom: 'VARIABLE',
+ },
+ {
+ kind: 'category',
+ name: 'Functions',
+ categorystyle: 'procedure_category',
+ custom: 'PROCEDURE',
+ },
+ ],
+};
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/complete-code/webpack.config.js b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/webpack.config.js
new file mode 100644
index 00000000000..1a095648fd7
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/complete-code/webpack.config.js
@@ -0,0 +1,59 @@
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+// Base config that applies to either development or production mode.
+const config = {
+ entry: './src/index.js',
+ output: {
+ // Compile the source files into a bundle.
+ filename: 'bundle.js',
+ path: path.resolve(__dirname, 'dist'),
+ clean: true,
+ },
+ // Enable webpack-dev-server to get hot refresh of the app.
+ devServer: {
+ static: './build',
+ },
+ module: {
+ rules: [
+ {
+ // Load CSS files. They can be imported into JS files.
+ test: /\.css$/i,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
+ plugins: [
+ // Generate the HTML index page based on our template.
+ // This will output the same index page with the bundle we
+ // created above added in a script tag.
+ new HtmlWebpackPlugin({
+ template: 'src/index.html',
+ }),
+ ],
+};
+
+module.exports = (env, argv) => {
+ if (argv.mode === 'development') {
+ // Set the output path to the `build` directory
+ // so we don't clobber production builds.
+ config.output.path = path.resolve(__dirname, 'build');
+
+ // Generate source maps for our code for easier debugging.
+ // Not suitable for production builds. If you want source maps in
+ // production, choose a different one from https://webpack.js.org/configuration/devtool
+ config.devtool = 'eval-cheap-module-source-map';
+
+ // Include the source maps for Blockly for easier debugging Blockly code.
+ config.module.rules.push({
+ test: /(blockly[/\\].*\.js)$/,
+ use: [require.resolve('source-map-loader')],
+ enforce: 'pre',
+ });
+
+ // Ignore spurious warnings from source-map-loader
+ // It can't find source maps for some Closure modules and that is expected
+ config.ignoreWarnings = [/Failed to parse source map.*blockly/];
+ }
+ return config;
+};
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/creating-the-block.mdx b/packages/docs/docs/codelabs/validation-and-warnings/creating-the-block.mdx
new file mode 100644
index 00000000000..68b1b9016cd
--- /dev/null
+++ b/packages/docs/docs/codelabs/validation-and-warnings/creating-the-block.mdx
@@ -0,0 +1,91 @@
+---
+description: Creating the custom block that we'll add validation to.
+---
+
+import Image from '@site/src/components/Image';
+
+# Block validation and warnings
+
+## 3. Create a block to validate
+
+Before we can add any validation, we need a custom block to validate. In this section you'll create a `list_range` block that generates a list of numbers counting up within a range, make it available in the toolbox, and define how it generates JavaScript code. The following sections will then build validation on top of it.
+
+### Define a custom block type
+
+Let's define a new custom block type named `list_range` with two number fields called `FIRST` and `LAST`. Open `src/blocks/text.js`. It already defines an `add_text` block; add the following `listRange` definition below it:
+
+```js
+// A custom block that generates a list of numbers counting up from FIRST to
+// LAST. We'll add validation to it over the course of this codelab.
+const listRange = {
+ type: 'list_range',
+ message0: 'create list of numbers from %1 up to %2',
+ args0: [
+ {
+ type: 'field_number',
+ name: 'FIRST',
+ value: 0,
+ },
+ {
+ type: 'field_number',
+ name: 'LAST',
+ value: 5,
+ },
+ ],
+ output: 'Array',
+ style: 'list_blocks',
+};
+```
+
+Then include the new block in the array passed to `createBlockDefinitionsFromJsonArray` at the bottom of the file, so that its definition is created alongside `add_text`:
+
+```js
+export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
+ addText,
+ listRange,
+]);
+```
+
+Next, to make this block available from the toolbox, open `src/toolbox.js`, find the `Lists` category, and insert this code at the beginning of that category's `contents`, right before the `lists_create_with` block:
+
+```js
+ {
+ kind: 'block',
+ type: 'list_range',
+ },
+```
+
+Now, if you refresh the running page (or run `npm start`) and open the `Lists` category in the toolbox, you should see the new block at the top:
+
+
+
+### Generating JavaScript code for the custom block
+
+You can drag this block out from the toolbox into the workspace, but if you try to use it, you'll find that Blockly doesn't know how to generate JavaScript code from this block yet, and error messages will appear in the browser console when it tries to update the display of the generated code. To fix this, open `src/generators/javascript.js` and add the following generator below the existing `add_text` generator:
+
+```js
+forBlock['list_range'] = function (block, generator) {
+ const first = block.getFieldValue('FIRST');
+ const last = block.getFieldValue('LAST');
+ const numbers = [];
+ for (let i = first; i <= last; i++) {
+ numbers.push(i);
+ }
+ const code = '[' + numbers.join(', ') + ']';
+ return [code, Order.NONE];
+};
+```
+
+Refresh the running page once again and try adding the new block to the workspace. You should be able to see it successfully generate JavaScript code this time. Try combining it with other blocks to see what the code might look like, and watch the generated code and output update automatically as you edit the workspace:
+
+
+
+Now we are ready to start adding validation!
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/displaying-warnings.mdx b/packages/docs/docs/codelabs/validation-and-warnings/displaying-warnings.mdx
index 8616484aa13..83fdd5cfcdc 100644
--- a/packages/docs/docs/codelabs/validation-and-warnings/displaying-warnings.mdx
+++ b/packages/docs/docs/codelabs/validation-and-warnings/displaying-warnings.mdx
@@ -2,40 +2,60 @@
description: How to display a warning when a block fails validation.
---
+import Image from '@site/src/components/Image';
+
# Block validation and warnings
-## 4. Displaying warnings
+## 5. Displaying warnings
Both Blockly's built-in validators and custom validators are nice because they immediately correct any errors so that there should never be any interruption in the validity of the blocks in the workspace or in the validity of the code that it generates. This results in a smooth, pleasant experience for the user, and you should take advantage of these validators whenever possible.
However, there may be invalid conditions that can't be corrected automatically because it's ambiguous what the desired result is. For example, it doesn't make much sense for our custom block to have a `FIRST` field with a greater value than the `LAST` field, but it's not obvious which of the two fields is "wrong." The best we can do is warn the user about the problem, and let them decide how to fix it.
-In the case of our custom block, we want our extension to be notified whenever either field is updated, so that it can check both of the fields to determine whether the block is currently valid. We can set that up with a general change listener by adding this code inside the extension function after the custom validator:
+In the case of our custom block, we want our extension to be notified whenever either field is updated, so that it can check both of the fields to determine whether the block is currently valid.
+
+We can set that up with a general change listener by updating the code inside the extension function in `src/index.js`:
```js
-// Validate the entire block whenever any part of it changes,
-// and display a warning if the block cannot be made valid.
-this.setOnChange(function (event) {
- const first = this.getFieldValue('FIRST');
- const last = this.getFieldValue('LAST');
- const valid = first < last;
- this.setWarningText(
- valid
- ? null
- : `The first number (${first}) must be smaller than the last number (${last}).`,
- );
+Blockly.Extensions.register('list_range_validation', function () {
+ // Add custom validation.
+ this.getField('LAST').setValidator(function (newValue) {
+ // Force an odd number.
+ return Math.round((newValue - 1) / 2) * 2 + 1;
+ });
+
+ // Validate the entire block whenever any part of it changes,
+ // and display a warning if the block cannot be made valid.
+ this.setOnChange(function (event) {
+ const first = this.getFieldValue('FIRST');
+ const last = this.getFieldValue('LAST');
+ const valid = first < last;
+ this.setWarningText(
+ valid
+ ? null
+ : `The first number (${first}) must be smaller than the last number (${last}).`,
+ );
+ });
});
```
This change listener function will get called whenever any part of the block is updated. It has access to all of the block's current field values, as well as the block's parents and children, if any. In this case, it reads both field values and compares them to determine whether the block is valid. Then it calls `this.setWarningText(...)`, which can accept either `null` indicating that there is nothing wrong or a string describing the problem.
-Reload `index.html`, drag the custom block into the workspace, then edit the fields to make the `FIRST` field greater than the `LAST` field. You should see a warning indicator, and you can click on it to see the warning message:
+Refresh the running page, drag the custom block into the workspace, then edit the fields to make the `FIRST` field greater than the `LAST` field. You should see a warning indicator, and you can click on it to see the warning message:
-
+
Depending on the severity of the issue, this might be sufficient for your custom block's needs. However, if the validity issue is severe enough that it wouldn't make sense to even try to generate code from the block, you should disable the block in addition to displaying a warning, because disabling a block makes code generators pretend the block doesn't exist. For example, most languages only allow `break` or `continue` statements inside of loops, so Blockly's corresponding built-in blocks are automatically disabled when used outside of loops:
-
+
You can disable a block using `this.setDisabledReason(true, 'reason')`, although there are some caveats: disabled blocks can't be dragged out of the toolbox flyout, and the act of disabling a block usually adds an event to Blockly's undo history. That's probably not the behavior you want when validating a block, so you can avoid both of these effects with the following code, which you should put inside the change listener function after setting the warning text:
@@ -51,6 +71,10 @@ if (!this.isInFlyout) {
}
```
-Reload `index.html` one last time, drag out the custom block, and edit the fields to make the `FIRST` field greater than the `LAST` field again. This time, the block should be disabled, and it won't generate code even if it is combined with other blocks:
+Refresh the running page one last time, drag out the custom block, and edit the fields to make the `FIRST` field greater than the `LAST` field again. This time, the block should be disabled, and it won't generate code even if it is combined with other blocks:
-
+
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/setup.mdx b/packages/docs/docs/codelabs/validation-and-warnings/setup.mdx
index 5d5533b2819..4e8f48ea3f6 100644
--- a/packages/docs/docs/codelabs/validation-and-warnings/setup.mdx
+++ b/packages/docs/docs/codelabs/validation-and-warnings/setup.mdx
@@ -2,111 +2,42 @@
description: Setting up the "Block validation and warnings" codelab.
---
-import Image from '@site/src/components/Image';
-
# Block validation and warnings
## 2. Setup
-### Download the sample code
-
-You can get the sample code for this codelab by either downloading the zip here:
-
-[Download zip](https://github.com/RaspberryPiFoundation/blockly/archive/main.zip)
-
-or by cloning this git repo:
-
-```bash
-git clone https://github.com/RaspberryPiFoundation/blockly.git
-```
-
-If you downloaded the source as a zip, unpacking it should give you a root folder named `blockly-main`.
-
-The relevant files are in `packages/docs/docs/codelabs/validation-and-warnings`. There are two versions of the app:
-
-- `starter-code/`: The starter code that you'll build upon in this codelab.
-- `complete-code/`: The code after completing the codelab, in case you get lost or want to compare to your version.
-
-Each folder contains:
+### Create the application
-- `index.js` - The codelab's logic. To start, it just injects a simple workspace.
-- `index.html` - A web page containing a simple blockly workspace, an empty space where generated code will be displayed, and a button to execute the generated code.
+Use the [`npx @blockly/create-package app`](https://www.npmjs.com/package/@blockly/create-package) command to create a standalone application that contains a sample setup of Blockly, including custom blocks and a display of the generated code and output.
-Open the file `starter-code/index.html` in a browser to see what it looks like. You should see a Blockly workspace with a toolbox, and space below it for generated code.
+1. Run `npx @blockly/create-package app validation-and-warnings-codelab` in your terminal. This will create a Blockly application in the folder `validation-and-warnings-codelab`.
+1. Use `cd` to move into the new directory: `cd validation-and-warnings-codelab`.
+1. Run `npm start` to start the server and run the sample application.
+1. The sample app will automatically run in the browser window that opens.
-
+The local address in the address bar of your browser starting with `http://localhost:` shows where your app is.
+If you accidentally close this window or tab, you can re-open this local address to view the app as long as `npm start` is still running.
-Next, open the file `starter-code/index.js` in a text editor. You will be making changes to this file, but first let's take a look at the contents. The code in this file already does a few things:
+### Change the storage key
-1. It uses Blockly's toolbox JSON API to define a toolbox containing a few built-in blocks that will be useful for testing our custom block.
-1. In a function called `start()`, it initializes a Blockly workspace with the above toolbox, and adds a change event listener that displays the generated JavaScript code whenever a block is moved or updated in the workspace.
-1. In a function called `executeCode()`, it executes the generated JavaScript code.
+Before setting up the rest of the application, **change the storage key** used for this codelab application. This will ensure that the workspace is saved in its own storage, separate from the regular sample app, so that it doesn't interfere with other demos.
-### Define a custom block type
-
-To prepare for adding validation, let's define a new custom block type named `list_range` with two number fields called `FIRST` and `LAST`. Copy the following code to the beginning of `index.js`:
-
-```js
-// Use Blockly's custom block JSON API to define a new block type.
-Blockly.common.defineBlocksWithJsonArray([
- {
- type: 'list_range',
- message0: 'create list of numbers from %1 up to %2',
- args0: [
- {
- type: 'field_number',
- name: 'FIRST',
- value: 0,
- },
- {
- type: 'field_number',
- name: 'LAST',
- value: 5,
- },
- ],
- output: 'Array',
- style: 'list_blocks',
- },
-]);
-```
-
-Then, to make this block available from the toolbox, find the toolbox definition in `index.js` and insert this code at the beginning of the list of available blocks, right before the one named `controls_forEach`:
+In `src/serialization.js`, change the value of `storageKey` to some unique string. `validationWorkspace` will work:
```js
- {
- kind: 'block',
- type: 'list_range',
- },
+// Use a unique storage key for this codelab
+const storageKey = 'validationWorkspace';
```
-Now, if you reload `index.html` and open the toolbox, you should see the new block at the top:
+### Explore the starter code
-
-
-### Generating JavaScript code for the custom block
-
-You can drag this block out from the toolbox into the workspace, but if you try to use it, you'll find that Blockly doesn't know how to generate JavaScript code from this block yet and error messages will appear in the browser console when it tries to update the display of the generated code. To fix this, add the following code below the custom block definition:
-
-```js
-// Define how to generate JavaScript from the custom block.
-javascript.javascriptGenerator.forBlock['list_range'] = function (block) {
- const first = this.getFieldValue('FIRST');
- const last = this.getFieldValue('LAST');
- const numbers = [];
- for (let i = first; i <= last; i++) {
- numbers.push(i);
- }
- const code = '[' + numbers.join(', ') + ']';
- return [code, javascript.Order.NONE];
-};
-```
+Take a moment to explore the code you just created in the `validation-and-warnings-codelab` folder. If you'd like a more in-depth explanation of all of the code in the sample app, you can go through the [getting started codelab](/codelabs/getting-started/codelab-overview) which goes through creating this sample app from scratch.
-Reload `index.html` once again and try adding the new block to the workspace. You should be able to see it successfully generate JavaScript code this time. Try combining it with other blocks to see what the code might look like, and click the button labeled "Execute the JavaScript code below" to see what happens when you run it:
+For a briefer outline of the code, visit the [sample app overview](/guides/get-started/sample-app-overview/) page for an overview of the included files and tooling.
-
+The pieces that matter most for this codelab are:
-Now we are ready to start adding validation!
+- `src/blocks/text.js` - defines the app's custom blocks using Blockly's JSON API. This is where we'll add our new block.
+- `src/generators/javascript.js` - defines how to generate JavaScript code from the custom blocks.
+- `src/toolbox.js` - defines which blocks are available in the toolbox.
+- `src/index.js` - registers the blocks and generators, injects the workspace, and re-runs the generated code whenever the workspace changes.
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/starter-code/index.html b/packages/docs/docs/codelabs/validation-and-warnings/starter-code/index.html
deleted file mode 100644
index f6ca51f2a12..00000000000
--- a/packages/docs/docs/codelabs/validation-and-warnings/starter-code/index.html
+++ /dev/null
@@ -1,55 +0,0 @@
-
-
-
-
- Validation and Warnings Codelab
-
-
-
-
-
-
-
-
Validation and Warnings Codelab
-
-
-
-
-
-
-
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/starter-code/index.js b/packages/docs/docs/codelabs/validation-and-warnings/starter-code/index.js
deleted file mode 100644
index 58c4706fc76..00000000000
--- a/packages/docs/docs/codelabs/validation-and-warnings/starter-code/index.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// Define which blocks are available in the toolbox.
-const toolbox = {
- kind: 'categoryToolbox',
- contents: [
- {
- kind: 'category',
- name: 'Blocks',
- categorystyle: 'list_category',
- contents: [
- {
- kind: 'block',
- type: 'controls_forEach',
- },
- {
- kind: 'block',
- type: 'math_on_list',
- },
- {
- kind: 'block',
- type: 'text_print',
- },
- {
- kind: 'block',
- type: 'controls_flow_statements',
- },
- ],
- },
- {
- kind: 'category',
- name: 'Variables',
- categorystyle: 'variable_category',
- custom: 'VARIABLE',
- },
- ],
-};
-
-let workspace = null;
-
-/**
- * Initialize a Blockly workspace, and add a change listener to update the display of generated code.
- *
- * Called from index.html when the page initially loads.
- */
-function start() {
- // Create main workspace.
- workspace = Blockly.inject('blocklyDiv', {
- toolbox: toolbox,
- });
-
- workspace.addChangeListener((event) => {
- const code = javascript.javascriptGenerator.workspaceToCode(workspace);
- document.getElementById('generatedCodeContainer').value = code;
- });
-}
-
-/**
- * Generate JavaScript code from the Blockly workspace, and execute it.
- *
- * Called from index.html when the execute button is clicked.
- */
-function executeCode() {
- const code = javascript.javascriptGenerator.workspaceToCode(workspace);
- eval(code);
-}
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/summary.mdx b/packages/docs/docs/codelabs/validation-and-warnings/summary.mdx
index 64784d649c8..933195e2e3b 100644
--- a/packages/docs/docs/codelabs/validation-and-warnings/summary.mdx
+++ b/packages/docs/docs/codelabs/validation-and-warnings/summary.mdx
@@ -5,7 +5,7 @@ description: Summary of the "Block validation and warnings" codelab.
# Block validation and warnings
-## 5. Summary
+## 6. Summary
In this codelab, you learned:
@@ -16,7 +16,7 @@ In this codelab, you learned:
- How to display a warning message on the block.
- How to disable a block (without adding an event to the undo history).
-You can find the code for the [completed custom block](https://github.com/RaspberryPiFoundation/blockly/docs/docs/codelabs/validation-and-warnings/complete-code/index.js) on GitHub.
+You can find the code for the [completed custom block](https://github.com/RaspberryPiFoundation/blockly/tree/main/packages/docs/docs/codelabs/validation-and-warnings/complete-code) on GitHub.
### Resources
diff --git a/packages/docs/docs/codelabs/validation-and-warnings/validating-blocks.mdx b/packages/docs/docs/codelabs/validation-and-warnings/validating-blocks.mdx
index 4038a7000d4..832d1e17692 100644
--- a/packages/docs/docs/codelabs/validation-and-warnings/validating-blocks.mdx
+++ b/packages/docs/docs/codelabs/validation-and-warnings/validating-blocks.mdx
@@ -4,59 +4,57 @@ description: How to validate the fields in a block.
# Block validation and warnings
-## 3. Validating blocks
+## 4. Validating blocks
When you're designing custom blocks, you may find that it doesn't make sense to use the block in certain ways. Depending on the intended purpose of your block, you may want to add constraints on the possible values that can be assigned to its fields, or on where it is used.
### Basic field constraints
-Blockly generally allows users to enter negative values and decimal values for number fields, but for this custom block, let's make sure that only positive whole numbers are allowed. Add `'min': 0` and `'precision': 1` to the fields in the custom block definition so that they look like this:
+Blockly generally allows users to enter negative values and decimal values for number fields, but for this custom block, let's make sure that only positive whole numbers are allowed. In `src/blocks/text.js`, add `min: 0` and `precision: 1` to the fields in the `listRange` definition so that they look like this:
```js
- args0: [
- {
- type: 'field_number',
- name: 'FIRST',
- value: 0,
- min: 0,
- precision: 1,
- },
- {
- type: 'field_number',
- name: 'LAST',
- value: 5,
- min: 0,
- precision: 1,
- },
- ],
+ args0: [
+ {
+ type: 'field_number',
+ name: 'FIRST',
+ value: 0,
+ min: 0,
+ precision: 1,
+ },
+ {
+ type: 'field_number',
+ name: 'LAST',
+ value: 5,
+ min: 0,
+ precision: 1,
+ },
+ ],
```
-Then reload `index.html`, drag the custom block to your workspace, and try entering various values such as negative numbers or decimals. Notice how invalid values are immediately converted to the nearest valid value!
+Then refresh the running page, drag the custom block to your workspace, and try entering various values such as negative numbers or decimals. Notice how invalid values are immediately converted to the nearest valid value!
-If you want, you can also add a `'max'` constraint to a number field.
+If you want, you can also add a `max` constraint to a number field.
### Adding custom validation to a field
-The built-in constraints are very convenient, but sometimes you might need to add custom constraints. For example, let's say that our custom block needs the first number of the range to be even, and the last number to be odd. We can easily implement the even constraint by setting the `'precision'` constraint of the `FIRST` field to `2`, but the odd constraint requires a custom validator.
+The built-in constraints are very convenient, but sometimes you might need to add custom constraints. For example, let's say that our custom block needs the first number of the range to be even, and the last number to be odd. We can easily implement the even constraint by setting the `precision` constraint of the `FIRST` field to `2`, but the odd constraint requires a custom validator.
So far, we've been using Blockly's JSON API for defining custom blocks, but Blockly also has a JavaScript API with more advanced features, and one of those features is defining custom validators. Fortunately, we don't have to convert our entire custom block definition to the JavaScript API in order to take advantage of these advanced features, because Blockly has a system for adding JavaScript extensions to blocks that were defined with the JSON API.
-Let's give our custom block a new extension called `list_range_validation`. Add `'extensions': ['list_range_validation']` to the end of the custom block definition in `index.js` like so:
+Let's give our custom block a new extension called `list_range_validation`. In `src/blocks/text.js`, add `extensions: ['list_range_validation']` to the end of the `listRange` definition like so:
```js
-Blockly.common.defineBlocksWithJsonArray([
- {
- type: 'list_range',
- ...
- style: 'list_blocks',
- extensions: [
- 'list_range_validation',
- ],
- },
-]);
+const listRange = {
+ type: 'list_range',
+ ...
+ style: 'list_blocks',
+ extensions: ['list_range_validation'],
+};
```
-Then copy the following code to `index.js` underneath the custom block definition to register an implementation of that extension:
+Then we need to register an implementation of that extension in `src/index.js`.
+
+Add the following code to `src/index.js` above the `// Register the blocks and generator with Blockly` comment:
```js
Blockly.Extensions.register('list_range_validation', function () {
@@ -72,7 +70,7 @@ this.getField('LAST').setValidator(function (newValue) {
});
```
-Blockly validator functions are called whenever the user enters a new value for that field, and the new value is passed to the function as a parameter. The parameter might be an invalid value, but validator function can return a valid value to override the user input. To force the `LAST` field to be an odd number, put the following code inside the validator function:
+Blockly validator functions are called whenever the user enters a new value for that field, and the new value is passed to the function as a parameter. The parameter might be an invalid value, but a validator function can return a valid value to override the user input. To force the `LAST` field to be an odd number, put the following code inside the validator function:
```js
return Math.round((newValue - 1) / 2) * 2 + 1;
@@ -90,4 +88,4 @@ Blockly.Extensions.register('list_range_validation', function () {
});
```
-Reload `index.html` now, then drag the custom block to your workspace and try setting the `LAST` field to various values. Notice how it always turns into an odd number!
+Refresh the running page now, then drag the custom block to your workspace and try setting the `LAST` field to various values. Notice how it always turns into an odd number!
diff --git a/packages/docs/sidebars.js b/packages/docs/sidebars.js
index b3f73202050..99d7f82c296 100644
--- a/packages/docs/sidebars.js
+++ b/packages/docs/sidebars.js
@@ -250,17 +250,22 @@ const sidebars = {
},
{
type: 'doc',
- label: '3. Validating blocks',
+ label: '3. Create a block to validate',
+ id: 'codelabs/validation-and-warnings/creating-the-block',
+ },
+ {
+ type: 'doc',
+ label: '4. Validating blocks',
id: 'codelabs/validation-and-warnings/validating-blocks',
},
{
type: 'doc',
- label: '4. Displaying warnings',
+ label: '5. Displaying warnings',
id: 'codelabs/validation-and-warnings/displaying-warnings',
},
{
type: 'doc',
- label: '5. Summary',
+ label: '6. Summary',
id: 'codelabs/validation-and-warnings/summary',
},
],
diff --git a/packages/docs/static/images/codelabs/validation-and-warnings/completed_toolbox.png b/packages/docs/static/images/codelabs/validation-and-warnings/completed_toolbox.png
index 7dcd0edb8ef..08a36b07231 100644
Binary files a/packages/docs/static/images/codelabs/validation-and-warnings/completed_toolbox.png and b/packages/docs/static/images/codelabs/validation-and-warnings/completed_toolbox.png differ
diff --git a/packages/docs/static/images/codelabs/validation-and-warnings/disabled_block.png b/packages/docs/static/images/codelabs/validation-and-warnings/disabled_block.png
index 97fa6f7ea19..b7a7b1d88fa 100644
Binary files a/packages/docs/static/images/codelabs/validation-and-warnings/disabled_block.png and b/packages/docs/static/images/codelabs/validation-and-warnings/disabled_block.png differ
diff --git a/packages/docs/static/images/codelabs/validation-and-warnings/disabled_break.png b/packages/docs/static/images/codelabs/validation-and-warnings/disabled_break.png
index 120b3658047..9c810e78fb6 100644
Binary files a/packages/docs/static/images/codelabs/validation-and-warnings/disabled_break.png and b/packages/docs/static/images/codelabs/validation-and-warnings/disabled_break.png differ
diff --git a/packages/docs/static/images/codelabs/validation-and-warnings/final_range_blocks.png b/packages/docs/static/images/codelabs/validation-and-warnings/final_range_blocks.png
index b50160d1bc9..24386a0d01d 100644
Binary files a/packages/docs/static/images/codelabs/validation-and-warnings/final_range_blocks.png and b/packages/docs/static/images/codelabs/validation-and-warnings/final_range_blocks.png differ
diff --git a/packages/docs/static/images/codelabs/validation-and-warnings/generated_javascript.png b/packages/docs/static/images/codelabs/validation-and-warnings/generated_javascript.png
index 3c65ead5be8..7db9dddfeeb 100644
Binary files a/packages/docs/static/images/codelabs/validation-and-warnings/generated_javascript.png and b/packages/docs/static/images/codelabs/validation-and-warnings/generated_javascript.png differ
diff --git a/packages/docs/static/images/codelabs/validation-and-warnings/starter_workspace.png b/packages/docs/static/images/codelabs/validation-and-warnings/starter_workspace.png
index abf842e57a8..95620c211c1 100644
Binary files a/packages/docs/static/images/codelabs/validation-and-warnings/starter_workspace.png and b/packages/docs/static/images/codelabs/validation-and-warnings/starter_workspace.png differ
diff --git a/packages/docs/static/images/codelabs/validation-and-warnings/warning_message.png b/packages/docs/static/images/codelabs/validation-and-warnings/warning_message.png
index dd51e8b8c29..4a35db9ce71 100644
Binary files a/packages/docs/static/images/codelabs/validation-and-warnings/warning_message.png and b/packages/docs/static/images/codelabs/validation-and-warnings/warning_message.png differ