Dynamically change dropdown based on selection from another dropdown

775 views
Skip to first unread message

sbalot

unread,
Nov 27, 2021, 10:08:16 AM11/27/21
to Blockly
Hello,

I am trying to implement a dynamic dropdown with options which update based on the selection of another drop down. 
For example: 

Each of the Ontology listed below has different dropdowns for the next field 'Select Elements'
img0.PNG
Here, BOT ontology has options Window, Door, Wall. 
img1.PNG
If I select 'SCHEMA' Ontology instead, then I want the dropdown of filed 'Select Element' to change. 

I read that this can be achieved using mutators, and event listeners. However, I am having trouble framing the right functions, since Javascript is not my strong suit :/ Can anyone help me on how to define the dynamic dropdown event listeners?


Beka Westberg

unread,
Nov 27, 2021, 12:30:32 PM11/27/21
to blo...@googlegroups.com
Hello!

So creating dynamic dropdowns that depend on other things (like other dynamic dropdowns) can be a bit tricky. There are two parts to it (1) creating the generator function, and (2) updating the second dropdown in response to the first.

(1) is pretty easy to implement, you just need to get the value of the other dropdown and return based on that. Here are the docs for dynamic dropdowns
```
// You can also use JSON (see docs) but I'm doing this for simplicity
Blockly.Blocks['my_block'] = {
  init: function() {
    // etc...
    this.appendDummyInput().appendField(new Blockly.FieldDropdown(this.generateOptions), 'SECOND_DROPDOWN'):
    // etc...
  }
  generateOptions: function() {
     // "this" in this context is the dropdown field.
     const otherVal = this.getSourceBlock().getFieldValue('FIRST_DROPDOWN');
     if (otherVal == 'BOT') return /* something */ ;
     if (otherVal == 'BEO') return /* something */ ;
     // etc...
  }
}
```

(2) is a bit trickier.  The point of this is to make sure you never end up with a value in the second dropdown which isn't compatible with the first. So whenever the first dropdown changes, you need to re-generate the options for the second (which is done by your generateOptions function) and then set the fields value to one of those options (usually the first). This uses a validator to listen for when the first dropdown changes.
```
Blockly.Blocks['my_block'] = {
  init: function() {
    // etc...
    this.appendDummyInput().appendField(new Blockly.FieldDropdown(/* my options */, this.validate), 'FIRST_DROPDOWN'):
    // etc...
  }
  validate: function() {
    var secondDropdown = this.getSourceBlock().getField('SECOND_DROPDOWN');
    var opts = secondDropdown.getOptions(false); // This regenerates the options for the dropdown
    secondDropdown.setValue(opts[0][1]);
  }
}
```

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

--
You received this message because you are subscribed to the Google Groups "Blockly" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blockly+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/blockly/2cef8e89-9191-4125-be9c-95a116d2d5d0n%40googlegroups.com.

fu6...@gmail.com

unread,
Nov 27, 2021, 8:27:03 PM11/27/21
to Blockly
Hi, share my sample code.

var list = [
["a","aaa"],
["a","abc"],
["b","bbb"],
["b","bcd"]
];

Blockly.Blocks["multidropdown_test"] = {
  init: function() {
    this.appendDummyInput()
        .appendField("Hello World");
    this.appendDummyInput()
        .appendField(new Blockly.FieldDropdown(this.getParentOptions, this.validate),"S1");
    this.appendDummyInput("second")
        .appendField(new Blockly.FieldDropdown([["",""]]),"S2");
    this.setInputsInline(true);
    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour(100); 
  },
  getParentOptions: function() {
     var opt = [];
for (var i=0;i<list.length;i++) {
for (var j=0;j<opt.length;j++) {
if (opt[j][0]==list[i][0]) {
opt.splice(j, 1);
break;
}
}
opt.push([list[i][0],list[i][0]]);
}
return opt;
  },
  validate: function(newValue) {
const sourceBlock = this.getSourceBlock();
         var opt = [];
for (var i=0;i<list.length;i++) {
if (list[i][0]==newValue) {
for (var j=0;j<opt.length;j++) {
if (opt[j][1]==list[i][1]) {
opt.splice(j, 1);
break;
}
}  
opt.push([list[i][1],list[i][1]]);
}
}
sourceBlock.getInput("second").removeField("S2");
sourceBlock.getInput("second").appendField(new Blockly.FieldDropdown(opt),"S2");
  }  
};
myMultiDropdown.mp4
Message has been deleted

fu6...@gmail.com

unread,
Nov 28, 2021, 1:04:53 AM11/28/21
to Blockly


var list = [
["a","a1","a11"],
["a","a1","a12"],
["a","a2","a21"],
["a","a2","a22"],
["b","b1","b11"],
["b","b1","b12"],
["b","b2","b21"],
["b","b2","b22"]
];

Blockly.Blocks["multidropdown_test"] = {
  init: function() {
    this.appendDummyInput()
        .appendField("Hello World");
    this.appendDummyInput()
        .appendField(new Blockly.FieldDropdown(this.getParentOptions, this.validate1),"S1");
    this.appendDummyInput("second")
        .appendField(new Blockly.FieldDropdown([["",""]], this.validate2),"S2");
    this.appendDummyInput("third")
        .appendField(new Blockly.FieldDropdown([["",""]]),"S3");
    this.setInputsInline(true);
    this.setPreviousStatement(true, null);
    this.setNextStatement(true, null);
    this.setColour(100); 
  },
  getParentOptions: function() {
var opt = [];
for (var i=0;i<list.length;i++) {
for (var j=0;j<opt.length;j++) {
if (opt[j][0]==list[i][0]) {
opt.splice(j, 1);
break;
}
}
opt.push([list[i][0],list[i][0]]);
}
if (opt.length==0)
opt.push(["",""]);   
return opt;
  },
  validate1: function(newValue) {
const sourceBlock = this.getSourceBlock();
var opt = [];
for (var i=0;i<list.length;i++) {
if (list[i][0]==newValue) {
for (var j=0;j<opt.length;j++) {
if (opt[j][1]==list[i][1]) {
opt.splice(j, 1);
break;
}
}
opt.push([list[i][1],list[i][1]]);
}
}
if (opt.length==0)
opt.push(["",""]);
sourceBlock.getInput("second").removeField("S2");
sourceBlock.getInput("second").appendField(new Blockly.FieldDropdown(opt, sourceBlock.validate2),"S2");
sourceBlock.validate2([newValue, sourceBlock.getFieldValue("S2")]);
  },
  validate2: function(newValue) {
if (!this.type) {
var sourceBlock = this.getSourceBlock();
var firstValue = sourceBlock.getFieldValue("S1");
}
else {
var sourceBlock = this;
var firstValue = newValue[0];
newValue = newValue[1];
}
var opt = [];
for (var i=0;i<list.length;i++) {
if (list[i][0]==firstValue&&list[i][1]==newValue) {
for (var j=0;j<opt.length;j++) {
if (opt[j][1]==list[i][2]) {
opt.splice(j, 1);
break;
}
}  
opt.push([list[i][2],list[i][2]]);
}
}
if (opt.length==0)
opt.push(["",""]);  
sourceBlock.getInput("third").removeField("S3");
sourceBlock.getInput("third").appendField(new Blockly.FieldDropdown(opt),"S3");
  }   
};
multidropdown3.mp4

sbalot

unread,
Nov 28, 2021, 8:13:42 AM11/28/21
to Blockly
Thanks you @beka and @fu6... for your quick reply. Based on what you had shared, I tried to frame my code as follows. But, this block does not get displayed at all when I open the index.html file. I guess I am making some mistake in connecting the generateOptions and validate functions to this block. Can you figure out where I am going wrong? 

BLOCKS.js
//Ontology block
Blockly.Blocks['ontology_block'] = {
  init: function() {
    this.appendDummyInput()
        .appendField("Ontology")
        .appendField(new Blockly.FieldDropdown([["BOT","BOT"], ["BEO","BEO"], ["SCHEMA","SCHEMA"]], this.validate), "FIRST_DROPDOWN");
    this.appendDummyInput()
        .appendField("Element")
    appendField(new Blockly.FieldDropdown(this.generateOptions), 'SECOND_DROPDOWN')
    this.setInputsInline(false);
    this.setNextStatement(true, null);
    this.setColour(180);
  this.setTooltip("");
  this.setHelpUrl("");
  },

  generateOptions: function() {
    const otherVal = this.getSourceBlock().getFieldValue('FIRST_DROPDOWN');
    if (otherVal=='BOT') {
      return 'Window'
    }
    if (otherVal=='BEO') {
      return'Wall'
    }
  },

  validate: function() {
    var secondDropdown = this.getSourceBlock().getField('SECOND_DROPDOWN');
    var opts = secondDropdown.getOptions(false);
    secondDropdown.setValue(opts[0][1]);
  }
};


GENERATOR.js
//Ontology
Blockly.JavaScript['ontology_block'] = function(block) {
  var dropdown_first_dropdown = block.getFieldValue('FIRST_DROPDOWN');
  var dropdown_second_dropdown = block.getFieldValue('SECOND_DROPDOWN');
  // TODO: Assemble JavaScript into code variable.
  // TODO: Assemble JavaScript into code variable.
  var code = dropdown_first_dropdown +':'+ dropdown_second_dropdown +'shape \n'+
              ' a sh:NodeShape;\n' +
              ' sh:targetClass ' + dropdown_first_dropdown+':'+dropdown_second_dropdown + ';\n'
  return code;
};

img2.PNG

sbalot

unread,
Nov 28, 2021, 8:16:21 AM11/28/21
to Blockly
Just realised that there was an error in the block.js file I had uploaded before. Below is the updated file. I still have the same error as before though. 

//Ontology block
Blockly.Blocks['ontology_block'] = {
  init: function() {
    this.appendDummyInput()
        .appendField("Ontology")
        .appendField(new Blockly.FieldDropdown([["BOT","BOT"], ["BEO","BEO"], ["SCHEMA","SCHEMA"]], this.validate), "FIRST_DROPDOWN");
    this.appendDummyInput()
        .appendField("Element")
        .appendField(new Blockly.FieldDropdown(this.generateOptions), 'SECOND_DROPDOWN')
    this.setInputsInline(false);
    this.setNextStatement(true, null);
    this.setColour(180);
  this.setTooltip("");
  this.setHelpUrl("");
  },

  generateOptions: function() {
    const otherVal = this.getSourceBlock().getFieldValue('FIRST_DROPDOWN');
    if (otherVal=='BOT') {
      return 'Window'
    }
    if (otherVal=='BEO') {
      return'Wall'
    }
  },

  validate: function() {
    var secondDropdown = this.getSourceBlock().getField('SECOND_DROPDOWN');
    var opts = secondDropdown.getOptions(false);
    secondDropdown.setValue(opts[0][1]);
  }
};

fu6...@gmail.com

unread,
Nov 28, 2021, 10:43:20 AM11/28/21
to Blockly
The code is provided for your reference.


Blockly.Blocks['ontology_block'] = {
  init: function() {
    this.appendDummyInput()
        .appendField("Ontology")
        .appendField(new Blockly.FieldDropdown([["BOT","BOT"], ["BEO","BEO"], ["SCHEMA","SCHEMA"]], this.validate), "FIRST_DROPDOWN");
    this.appendDummyInput('SECOND')
        .appendField("Element")
.appendField(new Blockly.FieldDropdown([['','']]), 'SECOND_DROPDOWN')
    this.setInputsInline(false);
    this.setNextStatement(true, null);
    this.setColour(180);
    this.setTooltip("");
    this.setHelpUrl("");
  },

  validate: function(newValue) {
if (newValue=='BOT')
var opt = [['Window','Window'], ["aaa","aaa"]];
else if (newValue=='BEO')
var opt = [['Wall','Wall'], ["bbb","bbb"]];
else if (newValue=='SCHEMA')
var opt = [['ccc','ccc'], ["ddd","ddd"]];
else
var opt = [['','']];

this.getSourceBlock().getInput("SECOND").removeField("SECOND_DROPDOWN");
this.getSourceBlock().getInput("SECOND").appendField(new Blockly.FieldDropdown(opt),"SECOND_DROPDOWN");
  }
};

Jason Schanker

unread,
Nov 28, 2021, 1:06:42 PM11/28/21
to Blockly
Hi,

Regarding the errors with the code:

appendField(new Blockly.FieldDropdown(this.generateOptions), 'SECOND_DROPDOWN')

needs to have a . before it so it's called on the input it's being appended to.

 const otherVal = this.getSourceBlock().getFieldValue('FIRST_DROPDOWN'); 

would work every time generateOptions is called except the first before the field is mounted on the block.  In this case this.getSourceBlock() will be null so trying to get the getFieldValue property causes the program to crash.  You can easily correct this by making sure this.getSourceBlock() is not null and giving it a default option value otherwise as follows:

     const otherVal = this.getSourceBlock() && this.getSourceBlock().getFieldValue('FIRST_DROPDOWN') || 'BOT';

Next, your generateOptions function needs to return an Array of option pairs.  For each pair, the first specifies what the user will see for the text of the option whereas the second will specify the value you get from getFieldValue.  So, for example, instead of return 'Window' for a 'BOT' selected value, you would use something like this:

if (otherVal === 'BOT') {
  return [["Window", "WINDOW"], ["Door", "DOOR"], ["Wall", "WALL"]];
}

So incorporating these changes, your code would look like this:

Blockly.Blocks['ontology_block'] = {
  init: function() {
    this.appendDummyInput()
        .appendField("Ontology")
        .appendField(new Blockly.FieldDropdown([["BOT","BOT"], ["BEO","BEO"], ["SCHEMA","SCHEMA"]], this.validate), "FIRST_DROPDOWN");
    this.appendDummyInput()
        .appendField("Element")
        .appendField(new Blockly.FieldDropdown(this.generateOptions), 'SECOND_DROPDOWN')
    this.setInputsInline(false);
    this.setNextStatement(true, null);
    this.setColour(180);
    this.setTooltip("");
    this.setHelpUrl("");
  },

  generateOptions: function() {
    const otherVal = this.getSourceBlock() && this.getSourceBlock().getFieldValue('FIRST_DROPDOWN') || 'BOT';
    if (otherVal === 'BOT') {
      return [["Window", "WINDOW"], ["Door", "DOOR"], ["Wall", "WALL"]];
    } 
    if (otherVal=='BEO') {
      return [["Wall", "WALL"], ["Something", "SOMETHING"], ["foo", "FOO"]];
    }
  },

  validate: function() {
    var secondDropdown = this.getSourceBlock().getField('SECOND_DROPDOWN');
    var opts = secondDropdown.getOptions(false);
    secondDropdown.setValue(opts[0][1]);
  }
};

This will get it to work (for BOT and BEO, you'll need to add the other options), but when you switch from say BOT to BEO, you'll notice that it gives you BOT's first option of Window.  This is because the first dropdown's value won't be set to the value of the newly selected option until *after* validation.  What this means is that when you switch from BOT to BEO, the validate function will call secondDropdown.getOptions(false), which will in turn call generateOptions as you'd want it to.  However, this.getSourceBlock().getFieldValue('FIRST_DROPDOWN') will still be 'BOT' so you still get the options of Window, Door, and Wall.  Then the secondDropdown will have its value set to Window because of (secondDropdown.setValue(opts[0][1])).  However, if you click the dropdown, you'll get the correct options because after validation, otherVal will correctly be BEO.

So, there are different approaches you can use here.  As shown in fu6's code, you can remove the second dropdown and add one with a new set of options every time the first dropdown's selected option changes.  Another possibility is to attach a property to your block that you use for the menu generator and that's changed in your validate function as follows:

Blockly.Blocks['ontology_block'] = {
  init: function() {
    this.currentValue = 'BOT';
    this.appendDummyInput()
        .appendField("Ontology")
        .appendField(new Blockly.FieldDropdown([["Bot","BOT"], ["Beo","BEO"], ["Schema","SCHEMA"]], this.validate.bind(this)), "FIRST_DROPDOWN");
    this.appendDummyInput()
        .appendField("Element")
        .appendField(new Blockly.FieldDropdown(this.generateOptions.bind(this)), 'SECOND_DROPDOWN')
    this.setInputsInline(false);
    this.setNextStatement(true, null);
    this.setColour(180);
    this.setTooltip("");
    this.setHelpUrl("");
  },

  generateOptions: function() {
    // this now refers to the block when it's called on the field dropdown because this was bound in init to the block 
    const otherVal = this.currentValue;
    if (otherVal === 'BOT') {
      return [["Window", "WINDOW"], ["Door", "DOOR"], ["Wall", "WALL"]];
    } 
    if (otherVal=='BEO') {
      return [["Wall", "WALL"], ["Something", "SOMETHING"], ["foo", "FOO"]];
    }
    if (otherVal=='SCHEMA') {
      return [["Table", "TABLE"], ["Something else", "SOMETHING ELSE"], ["bar", "BAR"]];
    }
  },

  validate: function(newValue) {
    this.currentValue = newValue;
    var secondDropdown = this.getField('SECOND_DROPDOWN');
    var opts = secondDropdown.getOptions(false); // This regenerates the options for the dropdown
    secondDropdown.setValue(opts[0][1]);
  }
};

Best,
Jason

sbalot

unread,
Nov 28, 2021, 7:04:35 PM11/28/21
to Blockly
Thank you for your inputs! I tried both of the solutions, but, I seem to have found the problem. It seems like neither 'this.getSourceBlock' nor 'secondDropdown.getOptions()' are being recognised. Attaching the error screen belowimg3.PNG

sbalot

unread,
Nov 28, 2021, 7:24:28 PM11/28/21
to Blockly
I found the error!! There were some issues with my blockly_compressed.js file. I think it must have been an older version. I updated it to the latest version available on the official github, and now it all works!! Thank you so much for all your help :) Very grateful :) 
Reply all
Reply to author
Forward
0 new messages