Mutator API
Published 4/3/20
If you have ever had problems with Blockly Mutators, you are not alone. The default UI is not very intuitive. Their applicability can be confusing. And the API makes them time consuming to create.
This post will present one example of a way to improve that last bit, the mutator API. It is not the only way, and it may not even be the best way. But I believe it does have some advantages.
Developer-Facing API
The basic idea is to create a group of objects that represent a kind of mutator. For example: an input
list mutator, a field list mutator, etc. These objects will be constructed, then passed to the block's
setMutator()
function, which will allow the object to create the UI. For example: a settings icon,
or a plus button.
This is essentially a strategy pattern where the mutator objects are the strategies. Using a strategy pattern makes the mutator system more extensible, and it makes it easier to share your mutators with other developers (they're pre-bundled!).
Making the mutator an object also allows it to bundle more than just UI. I think they should also include things like:
- Default serializers and deserializers
- Functions to make code generation easier
- And helpers for modifying the mutator programmatically
Mutator example
Let's take a look at how a developer might use this new API. In this example I'm going to use a ListMutator, because I think that's a common thing developers might want to create.
The mutator adds a + button to the block. Each time the + button is clicked a new input containing a - button is added. Each time a - button is clicked the input associated with that button is removed. If the last input is removed an "empty input" is added.
So you end up with a block that looks something like this:
To create the mutator you would do the below:
init: function() { // setStyle() etc... // Signature: // ListMutator(topInput, extraInput, // opt_emptyInput, opt_min, opt_max) // Each input is defined using JSON, // in the same way you define a block. var topInput = { "message0": "create list with %1", "args0": [ { "type": "input_value" "name": "ITEM" } ] }; var extraInput = { "message0": "%1", "args0": [ { "type": "input_value" "name": "ITEM" } ] }; var emptyInput = { "message0": "create empty list" }; var mutator = new ListMutator(topInput, extraInput, emptyInput, 0, 5); this.setMutator(mutator); }
Actually appending the UI, as well as serialization (i.e. domToMutation, and mutationToDom) are handled automatically for you.
Then you just create the code generator. Which would probably look something like:
function(block) { var mutator = block.mutator; var count = mutator.getCount(); if (count == 0) { return ['[]', Blockly.JavaScript.ORDER_NONE]; } var code = ''; for (var i = 0; i < count; i++) { // 'ITEM' matches the name of the input given above. var input = mutator.getInput('ITEM', i); // valueToCode would need to change to accept this. code += Blockly.JavaScript.valueToCode( input, Blockly.JavaScript.ORDER_NONE); } return [code, Blockly.JavaScript.ORDER_NONE]; }
You've just written a list mutator in approximately no seconds flat!
JSON
I also think that eventually this system could be transformed so that the block definition was JSON only. Instead of using an extension to construct the ListMutator, you would just pass the parameters via JSON:
{ "type": "my_list" // "style": etc... "mutator": { "type": "list_mutator", "topInput": { "message0": "create list with %1", "args0": [ { "type": "input_value" "name": "ITEM" } ] } "extraInput": { "message0": "%1", "args0": [ { "type": "input_value" "name": "ITEM" } ] } "emptyInput": { "message0": "create empty list" } "min": 0, "max": 5, } }
Note
One last note about this developer-facing API: You would still be able to use the old mutator system, or spin up your own. This is just meant to help you create common mutators more easily.
Implementation
Inheritance structure
The inheritance structure follows your basic strategy pattern.
setMutator
would just need to be changed to accept an IExtend
instead of a Blockly.Icon
. Then
developers could create whatever mutator system they wanted in a flexible way.
ListMutator
Implementing the concrete ListMutator presents some challenges. Based on discussion from the Blockly forums it seems like developers want to be able to access list items via their index. This is troublesome because Blockly requires each input to have a unique name.
I believe to achieve this and to allow any input to be removed at any time, each input should have a UUID attached to their name. This UUID should be put an array, so it can be accessed by index and used to get the input at said index. The code would look something like this:
getIndex = function(name, index) { var uuid = this.ids[index]; return this.sourceBlock.getInput(name + uuid); }
Which is possible because the mutator will handle appending all of the block's inputs.
Conclusion
Again this is just one example of how the mutator API could be changed. But I do hope that you found some of these ideas interesting, or maybe even useful!