Mutator help please!

440 views
Skip to first unread message

Laxmikant Sharma

unread,
Apr 3, 2020, 1:22:57 PM4/3/20
to Blockly
Hi Blockly team,

Need your help in understanding how to implement the mutator part. Your help would highly appreicated.

1. I created a block where i already have first and second dropdown. Based on first dropdown selection, second dropdown value changes, it works
2. Now the requirement if if I select 'Ranges' from second dropdown, i would like to extend the block automatically to also have two more dropdowns 'rangeList' and 'rangeOptionList'. 
3. I tried implementing the mutator but not able to do it properly due to lack of knowledge :( 
4. When I click on toolbox, I see some updated block (which i can correct after removing input and adding that to mutator part). But once i select it, it does not work.
5. Here is my toolbox category in my html file:

<category name="MyProject" color="150">
<block type="PropertyUpdate">
<mutation property_input="Ranges"></mutation>
<field name="property_input">Ranges</field>
</block>
</category>

here is JS code:

var myElements = ["Button-1", "Dropdown-1", "InputField-1", "Button-2", "Text-1"];

const propList = [
['Label', 'Label'],
['BackgroundColor', 'BackgroundColor'],
['Ranges', 'Ranges']
];

const rangeList = [
['Range1', 'Range1'],
['Range2', 'Range2'],
['Range3', 'Range3']
]

const rangeOptionList = [
['MinValue', 'MinValue'],
['MaxValue', 'MaxValue']
]
Blockly.Blocks['PropertyUpdate'] = {
init: function() {
this.appendValueInput("then")
.setCheck(null)
.setAlign(Blockly.ALIGN_CENTRE)
.appendField("Update")
.appendField(new Blockly.FieldDropdown(this.generateElementList, this.elementSelectionListener), "element")
.appendField("Property")
.appendField(new Blockly.FieldDropdown(this.generatePropertyOptions, this.elementAttributeListener), "Property")
.appendField("To");
this.setNextStatement(true, null);
this.setPreviousStatement(true, null);
this.setMutator(new Blockly.Mutator(['property_update_mutator']));
this.setColour(230);
this.setTooltip("");
this.setHelpUrl("");
},

generateElementList : function(){
<<Removing this for better code reading>>
},
elementSelectionListener : function(newValue) {
<<Removing this for better code reading>>
},
generatePropertyOptions : function() {
<<Removing this for better code reading>>
},
getPropOptions : function(firstValue) {
<<Removing this for better code reading>>
}
mutationToDom: function() {
var container = Blockly.utils.xml.createElement('mutation');
var propertyInput = this.getFieldValue('Property');
container.setAttribute('property_input', propertyInput);
return container;
},
domToMutation: function(xmlElement) {
var propertyInput = xmlElement.getAttribute('property_input');
this.updateShape_(propertyInput);
},
updateShape_: function(propertyInput) {
// Add or remove a Value Input.
var inputExists = this.getInput('rangeNo');
window.alert(propertyInput + " and inputExists : " + inputExists);
if (propertyInput == "Ranges"){
if (!inputExists){
this.appendDummyInput("EMPTY")
.appendField(new Blockly.FieldDropdown(rangeList), "rangeNo")
.appendField(new Blockly.FieldDropdown(rangeOptionList), "rangeOption");
}
} else if (inputExists) {
this.removeInput('rangeNo');
this.removeInput('rangeOption');
}
}

};

Blockly.Constants.PROPERTY.PROPERTY_UPDATE_MUTATOR_EXTENSION = function() {
this.getField('Property3').setValidator(function(option) {
var propertyInput = option;
this.getSourceBlock().updateShape_(propertyInput);
});
};

Blockly.Extensions.registerMutator('property_update_mutator',
Blockly.Constants.PROPERTY.PROPERTY_UPDATE_MUTATOR_MIXIN,
Blockly.Constants.PROPERTY.PROPERTY_UPDATE_MUTATOR_EXTENSION);

Beka Westberg

unread,
Apr 3, 2020, 2:01:04 PM4/3/20
to Blockly
Hello,

This looks like a pretty simple fix! I believe your problem is with the Blockly.Mutator() constructor. It takes in an array of block-type names, not a registration ID. E.g. 'list_create_with_item', which is the name of a block. Not 'property_update_mutator' which is the ID of an extension.

Since you're jusing a JavaScript block definition, you actually don't need to register an extension. Extensions are only for JSON block definitions. Instead you can add your new functions directly to the block.

Blockly.Blocks['MyWonderfulBlock'] = {
  init
: function() {},
  myLovelyFunction
: function() {},
  mutationToDom
: function() {},
 
// etc..
}

The lists_create_with block doesn't deal with dropdowns, but it might give you an idea of how to organize your mutator functions.

I hope that helps! If you have any further questions please reply!
--Beka

Laxmikant Sharma

unread,
Apr 3, 2020, 2:19:40 PM4/3/20
to Blockly
Thanks Beka! 

Your solution nicely for dialog based icon and then adding the blocks.  

Actually my bad, if possible, I do not want to use the dialog icon and then add the blocks. Rather I would like to add them if I select 'Ranges' from the second dropdown field. That is where i am getting bit confused.

Thanks,
Lax

Beka Westberg

unread,
Apr 3, 2020, 2:42:17 PM4/3/20
to Blockly
Hello again Lax,

> I do not want to use the dialog icon and then add the blocks

Makes sense! Luckily this is easy to achieve as well.

Mutation has two parts: Serialization (mutationToDom and domToMutation) and UI. Calling setMutator() adds the dialog icon UI to the block. It does not have anything to do with serialization.

Since you want to handle the UI yourself (using dropdowns) just don't call setMutator. An example of this is the math_number_property block documented in the Mutators guide. It defines domToMutation and mutationToDom, but does not call setMutator.

I hope that helps! Again if you have any questions please reply :D
--Beka

Laxmikant Sharma

unread,
Apr 3, 2020, 3:56:22 PM4/3/20
to Blockly
My apologies Beka for coming back on this, i am still not able to get it.

Here is my simplified JS :

var myElementList = [["Button-1", "Button-1"], ["DropDown-1", "DropDown-1"]];

const propsList = [['Label', 'Label'],['Ranges', 'Ranges']];

const rangesList = [['Range1', 'Range1'],['Range2', 'Range2']];


Blockly.Blocks['PropertyUpdate2'] = {

  init: function() {

    this.appendValueInput("then")

        .setAlign(Blockly.ALIGN_CENTRE)

        .appendField("Update")

        .appendField(new Blockly.FieldDropdown(myElementList), "element4")

        .appendField("Property")

        .appendField(new Blockly.FieldDropdown(propsList), "Property4")

        .appendField("To");

        this.setNextStatement(true, null);

        this.setPreviousStatement(true, null);

    this.setColour(230);

  },


  mutationToDom: function() {

    var container = Blockly.utils.xml.createElement('mutation');

    var propertyInput = this.getFieldValue('Property4');

    container.setAttribute('property_input', propertyInput);

    return container;

  },


  domToMutation: function(xmlElement) {

    var propertyInput = xmlElement.getAttribute('property_input');

    this.updateShape_(propertyInput);

  },

  

  updateShape_: function(propertyInput) {

    var inputExists = this.getInput('rangeNo');

    if (propertyInput == "Ranges"){

      if (!inputExists){

        this.appendDummyInput("EMPTY")

            .appendField(new Blockly.FieldDropdown(rangesList), "rangeNo")

      }

    } else if (inputExists) {

      this.removeInput('rangeNo');

    }

  }  

};


Here is how i am adding this block in toolbox xml in my html sample page:

<category name="InatantAR" color="150">

      <block type="PropertyUpdate2">

        <mutation property_input="Ranges"></mutation>

        <field name="property_input">Ranges</field>

      </block>

    </category>


When I click on toolbox, it does show the additional dropdown field added to the block. But when I select the block, add it to blockly workspace, and select Ranges from second dropdown, I do not see mutationToDom or domToMutation being called. hence the additional blocks do not get added as updateShare_ is not called.


I must be missing something like registering the mutator or event or something? Here is how i am adding the workspace to my sample html file:


<script>

var workspace = Blockly.inject('blocklyDiv',

  {toolbox: document.getElementById('toolbox'),

   grid:

       {spacing: 20,

        length: 2,

        colour: '#ccc',

        snap: true},

  move:{

        scrollbars: true,

        drag: true,

        wheel: false},

   zoom:

         {controls: true,

          wheel: true,

          startScale: 1.0,

          maxScale: 3,

          minScale: 0.3,

          scaleSpeed: 1.2},

     trashcan: true});

    //workspace.addChangeListener(myUpdateFunction);

</script>


Please let me know where i am making the mistake.

Appreciate all your help!

Thanks,
Lax

Beka Westberg

unread,
Apr 3, 2020, 4:06:33 PM4/3/20
to Blockly
Hello Lax,

Your new version looks great! From my reading it looks like your second dropdown is missing the validator that was in the original version of your block. You can use setValidator() to set the validator like you did before. Or you can pass it directly to the dropdown constructor.

If that's not the issue please tell me! I'll pull it down and play with it :D
--Beka

Laxmikant Sharma

unread,
Apr 3, 2020, 4:10:16 PM4/3/20
to Blockly
Yes, I will add the validator but how do I force mutationToDom and domToMutation being called. Do I need to explicitly call these and updateShape_ from validator?

Laxmikant Sharma

unread,
Apr 3, 2020, 4:17:16 PM4/3/20
to Blockly
my stupidity, i think i can update the shape in validator itself! I tried and it worked! Thanks Beka! you are amazing.

Beka Westberg

unread,
Apr 3, 2020, 4:26:26 PM4/3/20
to Blockly
Yay I'm glad you've got something that works! And don't worry mutators are definitely confusing, it took me a quite while to get my head wrapped around them haha.

If you ever have more questions in the future definitely don't hesitate to post! There are a ton of brilliant people here that love helping out.

Happy coding and good luck on your project!
--Beka
Reply all
Reply to author
Forward
0 new messages