Mutators

A mutator is a special extension; in addition to changing the block, it defines how those changes will be saved to XML and loaded from XML. Non-mutator extensions may not save changes to or load changes from XML. Mutators on web 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 so 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 JSON
  • mixinObj: 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 = 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.

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.

JavaScript

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.

JavaScript

// 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.