Blockly Developer Tools lets you create custom blocks using blocks! It has support for fields that are published as plugins in addition to the fields that come with core Blockly. If you've created a custom field, you can add support for it to the Block Factory by following this guide. The custom field must be published on npm before you can add support for it. You also need to commit to updating your field to keep up with changes in Blockly, otherwise we may need to remove it from Block Factory in the future.
Development on the Block Factory
The source code for the Block Factory is located in the blockly-samples
repository in the examples/developer-tools
directory.
To submit a change to the Developer Tools in blockly-samples, you'll need to
follow the typical steps for developing in
blockly-samples. Unlike working with plugins however, you'll need to run npm
install
from the examples/developer-tools
directory directly, rather than at
the root level of blockly-samples.
Install the plugin
In order for the Block Factory to show your custom field in the preview, it
needs to install the custom field. Add your field as an npm dependency of
developer-tools. Then, register it or do any other setup work necessary in
developer-tools/src/blocks/index.ts
.
Create a block for the field
Since the Block Factory uses blocks to create custom blocks, you'll need a block that represents your custom field.
Create the block definition
You need to design the block for your field; if you want to get meta, you can
even design it using Block Factory! The block should allow the user to configure
the setup required by your field, such as default values, and a name. Add this
block definition to
developer-tools/src/blocks/fields.ts
, and import it in
developer-tools/src/blocks/index.ts
.
Add block to toolbox
Next, you need to add this block to the toolbox definition to make it accessible
to users. The toolbox definition is located in
developer-tools/src/toolbox.ts
. Your block should be added
to the "Fields" category.
Code Generators
The Block Factory works by using the Code Generator system you are already familiar with from Blockly. Each block has a block-code generator for each type of output generated by the Block Factory, and the parent blocks assemble the code for the child blocks into the correct output. To add support for a custom field, you'll need to add block-code generator functions for each of the Code Generator classes.
Create a file for your field block in the
output-generators/fields
directory. You'll add the
block-code generators for each of the following Generators to this file. Import
this file in the blocks/index.ts
file so that the
block-code generator functions are loaded into the application.
JavaScript definition
The javascriptDefinitionGenerator
creates the code that will be included in
the JavaScript definition for a block that includes your custom field. Usually,
this means the block-code generator should return a line of code that looks like
.appendField(new YourFieldConstructor(arg1, arg2), 'userSpecifiedName')
. Note
that this line of code does not include a semicolon, because an input that
contains multiple fields will have several calls to appendField
chained
together. The arguments in the constructor are pulled from the values the user
set on the field block. Here's an example of this block-code generator for
FieldAngle
:
javascriptDefinitionGenerator.forBlock['field_angle'] = function (
block: Blockly.Block,
generator: JavascriptDefinitionGenerator,
): string {
const name = generator.quote_(block.getFieldValue('FIELDNAME'));
const angle = block.getFieldValue('ANGLE');
return `.appendField(new FieldAngle(${angle}), ${name})`;
};
The angle block that the user dragged from the "Fields" category of the Block Factory toolbox has two fields on it:
FIELDNAME
: user can set the name of the field on their custom blockANGLE
: user can set the default angle value
In this block-code generator, we get the default angle value and pass it as the
only argument to the FieldAngle
constructor. The field name is always passed
as the second argument to appendField
.
JSON definition
The jsonDefinitionGenerator
is similar, but this outputs the part of the JSON
block definition that corresponds to your field. Typically, this code is a JSON
object that includes:
type
: corresponds to the name of your field in the Blockly field registryname
: user can set the name of the field on their custom block- any other custom properties needed by your field's json initialization method.
Here's an example from the FieldAngle
again:
jsonDefinitionGenerator.forBlock['field_angle'] = function (
block: Blockly.Block,
generator: JsonDefinitionGenerator,
): string {
const code = {
type: 'field_angle',
name: block.getFieldValue('FIELDNAME'),
angle: block.getFieldValue('ANGLE'),
};
return JSON.stringify(code);
};
Code headers
The Code Header Generator creates the Code Headers output shown in the Block
Factory. This output can be toggled between esmodule imports and script tags,
depending on how the user wishes to load the code, so there are actually two
different generator instances: one for each case. You need to add a block-code
generator for each of them. Here's an example for FieldAngle
:
importHeaderGenerator.forBlock['field_angle'] = function (
block: Blockly.Block,
generator: CodeHeaderGenerator,
): string {
generator.addHeaderLine(
`import {registerFieldAngle, FieldAngle} from '@blockly/field-angle';`,
);
generator.addHeaderLine(`registerFieldAngle();`);
return '';
};
scriptHeaderGenerator.forBlock['field_angle'] = function (
block: Blockly.Block,
generator: CodeHeaderGenerator,
): string {
generator.addHeaderLine(
`<script src="https://unpkg.com/@blockly/field-angle"></script>`,
);
generator.addHeaderLine(`registerFieldAngle();`);
return '';
};
These generators have a method called addHeaderLine
that lets you specify a
line of code that should be called before your field is used in code. Commonly,
this includes work such as importing the field or loading it through a script
tag, and perhaps calling a function that will register the field with Blockly's
field registry.
For these two block-code generators, all the code should be added through calls
to addHeaderLine
. This function will ensure each header line is only shown
once, even if your custom field block is used multiple times in one custom
block. The block-code generator should return the empty string.
Generator stub
Lastly, we have the generator that creates the generator stub for the field. In this block-code generator, you'll be writing code that generates code that helps the user write code that generates code. Confused yet? It's easier than it sounds!
The generator stub for a custom block includes a premade variable
representing every field on the block. Then there's a TODO
the user must
finish to assemble all of these variables into the final code string their
custom block will return. That means typically all your block-code generator
needs to do is return the line that creates this custom variable. Say the user
is making a custom block that will add rays of sunlight to their canvas. They
add an angle field to the block and name it "SUN_DIRECTION"
. The generator
stub for this block would include the line const angle_sun_direction =
block.getFieldValue("SUN_DIRECTION");
. That is the line of code our block-code
generator for the angle field needs to return:
generatorStubGenerator.forBlock['field_angle'] = function (
block: Blockly.Block,
generator: GeneratorStubGenerator,
): string {
const name = block.getFieldValue('FIELDNAME');
const fieldVar = generator.createVariableName('angle', name);
return `const ${fieldVar} = block.getFieldValue(${generator.quote_(
name,
)});\n`;
};
To get a standardized name for the variable, you can call
generator.createVariableName
and pass in the type of your field (such as
angle
, number
, etc.) along with what the user named their field.
Test it
After you've written all of these pieces, you should be able to start the Block
Factory by running npm start
in the blockly-samples/examples/developer-tools
directory. You should be able to drag your block from the field category, add it
to an input on a block, and watch the output change. Check that the preview of
the block looks right, and that the code for each of the output sections is
correct.