Extensions and Mutators

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