Advanced blocks may use a mutator to be even more dynamic and configurable.
Mutators allow blocks to change in custom ways, beyond dropdown and text input
fields. The most visible example is the pop-up dialog which allows if
statements to acquire extra else if
and else
clauses. But not all mutations
are so complex.
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"> <b><mutation divisor_input="true"></mutation></b> <field name="PROPERTY">DIVISIBLE_BY</field> </block>
Saving mutation data is done by adding a mutationToDom
function in the
block's definition. 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 is added to a block in its init
function.
this.setMutator(new Blockly.Mutator(['controls_if_elseif', 'controls_if_else']));
The setMutator
function takes one argument, a new Mutator. The Mutator
constructor takes one argument, a list of sub-blocks to show in the mutator's
toolbox. Creating a nested mutator for a mutator's sub-block is not advised at
this time.
When a mutator dialog is opened, the block's decompose
function is called to
populate the mutator's workspace.
decompose: function(workspace) { var topBlock = Blockly.Block.obtain(workspace, '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.
Ideally this function would ensure that any blocks already connected to the original block should remain connected to the correct inputs, even if the inputs are reordered.