Advanced blocks may use extensions or mutators to be even more dynamic and configurable.
Extensions allow programmatic configuration of blocks, extra
initialization, or custom behaviors to be added to blocks. For example, several
blocks use the
parent_tooltip_when_inline
extension to display their parent's tooltip when connected to another block.
A mutator is very similar to an extension; in addition to changing the block, it defines how those changes will be saved to XML and loaded from XML. Mutators may also have additional UI for the user to configure their state. The most recognizable mutator in Blockly is the if block.
Extensions
Extensions are custom configuration or behavior for blocks which can be applied
to a block through the block's
JSON definition. The
extensions for a block are added using the extensions
key. Multiple extensions
may be applied to a single block.
{
//...,
"extensions": ["break_warning_extension", "parent_tooltip_extension"],
}
Because an extension performs work beyond Blockly's default behavior, it must be written once for each platform being used. Each platform includes an API for registering the extension with Blockly. Each extension defines a function to run on block creation. Adding an extension to a block's "extensions" key says that the associated function should be run once on each new block of that type as it is created.
Each extension must be registered through a call to the Blockly library.
Blockly.Extensions.register('parent_tooltip_extension',
function() {
// this refers to the block that the extension is being run on
var thisBlock = this;
this.setTooltip(function() {
var parent = thisBlock.getParent();
return (parent && parent.getInputsInline() && parent.tooltip) ||
Blockly.Msg.MATH_NUMBER_TOOLTIP;
});
});
JavaScript also provides a convenience method for extensions that are only
a mixin, `Blockly.Extensions.registerMixin(name, mixinObj)`.
Mutators
Mutators are the only way to provide custom serializable state on a block. They are declared on a block's JSON definition using the mutator key. In addition to changing the block, a mutator defines how those changes will be saved to XML and loaded from XML. Only one mutator may be declared on a block.
{
//...,
"mutator": "controls_if_mutator",
}
Mutators are implemented using a collection of methods that are mixed in to a block at instantiation and an optional UI for the user to configure the mutation. A block may only have one set of mutator methods.
The most visible example of a mutator is the pop-up dialog which allows if
statements to acquire extra else if
and else
clauses. But not all mutations
are that complex.
Registering a mutation
Just like extensions, mutations must be registered with Blockly.Extensions. The Blockly library provides a convenience method that performs basic validation of the mutation and handles the standard configuration.
Blockly.Extensions.registerMutator(
name, mixinObj, opt_helperFn, opt_blockList);
name
: The string name of the mutator used in JSONmixinObj
: An object containing the various mutation methods.opt_helperFn
: An optional helper function that will run on the block after the mixin.opt_blockList
: An optional list of blocks to use with the default mutator editing UI.
Mixin object
Mutators on web are just a set of methods that are mixed in to the block's
object during initialization. At a minimum, a mutator on a block must add
mutationToDom
and domToMutation
which specify how to serialize and
deserialize the mutation state. Mutations that use the default mutator UI must
also implement decompose
and compose
to tell the UI how to explode a block
into sub-blocks and how to update the mutation from a set of sub-blocks.
The methods on the mixin object will be added to each block instance, so this
may be used to refer to the block.
mutationToDom and domToMutation
The XML format used to load, save, copy, and paste blocks automatically captures and restores all data stored in editable fields. However, if the block contains additional information, this information would be lost when the block is saved and reloaded. Each block's XML has an optional mutator element where arbitrary data may be stored.
A simple example of this is
math.js's
math_number_property
block. By default it has one input:
If the dropdown is changed to "divisible by", a second input appears:
This is easily accomplished with the use of a change handler on the dropdown
menu. The problem is that when this block is created from XML (as occurs
when displayed in the toolbox, cloned from the toolbox, copied and pasted,
duplicated, or loaded from a saved file) the init
function will build the
block in its default one-input shape. This results in an error if the XML
specifies that some other block needs to be connected to an input that does not
exist.
Solving this problem simply involves writing a note to the mutator element recording that this block has an extra input:
<block type="math_number_property">
<mutation divisor_input="true"></mutation>
<field name="PROPERTY">DIVISIBLE_BY</field>
</block>
Saving mutation data is done by adding a mutationToDom
function to the
mixinObj. Here is the example from the math_number_property
block:
mutationToDom: function() {
var container = document.createElement('mutation');
var divisorInput = (this.getFieldValue('PROPERTY') == 'DIVISIBLE_BY');
container.setAttribute('divisor_input', divisorInput);
return container;
}
This function is called whenever a block is being written to XML. If the function does not exist or returns null, then no mutation is recorded. If the function exists and returns a 'mutation' XML element, then this element (and any properties or child elements) will be stored at the beginning of the block's XML representation.
The inverse function is domToMutation
which is called whenever a block is
being restored from XML. Here is the example from the math_number_property
block:
domToMutation: function(xmlElement) {
var hasDivisorInput = (xmlElement.getAttribute('divisor_input') == 'true');
this.updateShape_(hasDivisorInput); // Helper function for adding/removing 2nd input.
}
If this function exists, it is passed the block's 'mutation' XML element. The function may parse the element and reconfigure the block based on the element's properties and child elements.
compose and decompose
Mutation dialogs allow a user to explode a block into smaller sub-blocks and
reconfigure them, thereby changing the shape of the original block. The dialog
button and the default editing UI is added to a block if both the compose
and
decompose
methods are defined on the mixinObj. If neither is defined no
mutator UI will be created, but events or other code may still cause a mutation.
Defining only one of these two functions is an error.
See Mutator editing UI for more details on the editing UI.
When a mutator dialog is opened, the block's decompose
function is called to
populate the mutator's workspace.
decompose: function(workspace) {
var topBlock = workspace.newBlock('controls_if_if');
topBlock.initSvg();
...
return topBlock;
}
At a minimum this function must create and initialize a top-level block for the mutator dialog, and return it. This function should also populate this top-level block with any sub-blocks which are appropriate.
When a mutator dialog saves its content, the block's compose
function is
called to modify the original block according to the new settings.
compose: function(topBlock) { ... }
This function is passed the top-level block from the mutator's workspace (the
same block that was created and returned by the compose
function). Typically
this function would spider the sub-blocks attached to the top-level block, then
update the original block accordingly.
saveConnections
Ideally the compose
function would ensure that any blocks already connected to
the original block remain connected to the correct inputs, even if the inputs
are reordered. To do this, define a saveConnections
method on your
mixinObj
:
/**
* Store pointers to any connected child blocks.
* @param {!Blockly.Block} containerBlock Root block in mutator.
* @this {Blockly.Block}
*/
saveConnections: function(containerBlock) {
...
}
If saveConnections
is defined, the mutator will call it before compose.
Helper function
Along with the mixin a mutator may register a helper function. This function is run on the block after it is instantiated and the mixinObj is added and can be used to add additional triggers or effects to a mutation.
One example is the math_is_divisibleby_mutator
in the
math blocks
which checks the dropdown and updates the block to have the correct number of
inputs.
Blockly.Constants.Math.IS_DIVISIBLE_MUTATOR_EXTENSION = function() { this.getField('PROPERTY').setValidator(function(option) { var divisorInput = (option == 'DIVISIBLE_BY'); this.sourceBlock_.updateShape_(divisorInput); }); }; Blockly.Extensions.registerMutator('math_is_divisibleby_mutator', Blockly.Constants.Math.IS_DIVISIBLEBY_MUTATOR_MIXIN, Blockly.Constants.Math.IS_DIVISIBLE_MUTATOR_EXTENSION);
Mutator editing UI
The mutator also needs UI if the user should be able to edit the block's shape.
The easiest way to add this is to implement
compose
and decompose
in your mixin and optionally
provide a list of blocks to include in the default editor.
Blockly.Extensions.registerMutator('controls_if_mutator',
Blockly.Constants.Logic.CONTROLS_IF_MUTATOR_MIXIN, null,
['controls_if_elseif', 'controls_if_else']);
In this case, Blockly will use the default mutator UI and allow the user to add
controls_if_elseif
and controls_if_else
blocks to the stack returned by
decompose
.
Custom editor UIs
If your app uses a custom mutator UI, you can also use the opt_helperFn
to
set the custom editor UI on the block with the setMutator
method.
// declare the helper function
var myMutatorFn = function() {
// this will refer to the block
this.setMutator(new MyMutator(...));
};
//...
// register the mutator along with the helper function
Blockly.Extensions.registerMutator('my_mutator', MY_MUTATOR_MIXIN,
myMutatorFn, null);
The setMutator
function takes one argument, a new Mutator. The default mutator
used by Blockly is implemented in
mutator.js.