Dynamic Dropdowns based on other blocks

588 views
Skip to first unread message

Lewis Newman

unread,
Feb 18, 2021, 8:50:11 AM2/18/21
to Blockly
Hi Everyone,

I've been working on a tool to generate html + css documents with google blockly. As part of this I've created a few blocks where they have a dropdown field that is dynamically updated based on the existence of other blocks. BlocklyCapture3.PNG
In order to achieve this I've used a function to generate the drop-down values however I have run into 2 issues with doing this:

1. If the user changes the name of the document that is being referenced, the dropdown value name doesn't currently change with it.

2. When saving the xmldom and reloading it later, some of the dropdowns forget their value and end up getting reset to none. This I believe is based on the order of the blocks in the workspace e.g. blocks at the top of the workspace get loaded first - therefore if their dropdown field is based on a block that is loaded after it - it gets forgotten.

I'd be grateful if anyone has any ideas about a better way to implement this, I'm currently wondering whether I should be using mutations for this perhaps.

This is the current code I've got for generating the dropdowns:
  generateOptions: function(){

    console.log("generateOptions");
    console.log(workspaces);

    var results = [["none", "none"]];

    // For each blockly workspace we have
    // workspaces is a global variable from elsewhere
    for(const [key, value] of Object.entries(workspaces)){

      // Get a list of all the css document types on this workspace
      var css_blocks = value.getBlocksByType("css_document");

      //Add the dropdown values for each block
      for(var j = 0; j < css_blocks.length; j++){
        var name = css_blocks[j].getFieldValue("NAME");
        results.push([name, name+".css"]);
      }


    }
    return results;  
  }


Thanks
Lewis

Beka Westberg

unread,
Feb 18, 2021, 5:48:30 PM2/18/21
to blo...@googlegroups.com
Hello Lewis,

> I'd be grateful if anyone has any ideas about a better way to implement this, I'm currently wondering whether I should be using mutations for this perhaps.
Yes this is definitely a case where you want to use mutators :D Usually you think of mutators as allowing you to change the shape of a block, but as mentioned in the documentation, mutators at their core are just a way to provide custom serialization for a type of block. Since your generator function for your dynamic dropdown isn't deterministic, you'll want to add custom serialization to account for that =)

Thank you for sharing about these blocks by the way! They look super cool :D Are you planning on publishing, or is this just a personal project? I'd love a chance to play with them!

Best wishes,
--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/fd77fa52-9e03-4f20-8e14-fac0f46a45f7n%40googlegroups.com.

Lewis Newman

unread,
Feb 19, 2021, 10:09:33 AM2/19/21
to Blockly
Hi Beka

Thanks for the help, mutators were what I needed. For anyone that's comes across this I used the mutators to load/save the dropdown options, and then modified the generateOptions function to use the list of saved options only when the blocks are loaded.

And yes I do plan on publishing this as a website at some point, only I have no idea quite when that will be at the moment, but I'll drop a link in here when I do.

Thanks again
Lewis
Message has been deleted

Beka Westberg

unread,
Feb 19, 2021, 3:33:10 PM2/19/21
to blo...@googlegroups.com
> And yes I do plan on publishing this as a website at some point, only I have no idea quite when that will be at the moment, but I'll drop a link in here when I do.

Awesome! I can't wait :D And thanks for adding info about how you solved your issue. I know future people always really appreciate that hehe

Best wishes,
Beka

Bart Butenaers

unread,
Jun 17, 2021, 2:40:37 PM6/17/21
to Blockly
Hi Lewis,
I have exactly the same problem.  
 
I used the mutators to load/save the dropdown options, and then modified the generateOptions function to use the list of saved options only when the blocks are loaded.

It would be very nice if you could share your code to get me started.  Your 'theory' sounds very logical, but not sure what would be the best way to implement it.

Thanks a lot!!!!
Bart

Bart Butenaers

unread,
Jun 21, 2021, 1:52:39 AM6/21/21
to Blockly
Hi everybody,

I am almost complete with my code, and I will share it afterwards here (in case Lewis hasn't responded yet).

 About: "I used the mutators to load/save the dropdown options, and then modified the generateOptions function to use the list of saved options only when the blocks are loaded."

So the domToMutation needs to store the loaded options (from the xml) into e.g. Blockly.loadedDropdownOptions, where the generateOptions can read the loaded options (until all blocks have been loaded).  However how does the generateOptions knows when all blocks have been loaded?

Thanks!!
Bart

Bart Butenaers

unread,
Jun 21, 2021, 6:51:44 AM6/21/21
to Blockly
BTW I am not sure how Lewis could implement this, because it seems that the finishedLoading event is not available yet...  So there must be another way to determine (in the generateOptions function) to determine when the workspace is loaded.  All tips are very welcome!

Lewis Newman

unread,
Jun 21, 2021, 7:07:10 AM6/21/21
to Blockly
Hi Bart

So I'm not completely sure how I did this since it was a couple of months since I looked at the code.

However I believe what I did was in mutationToDom, save the currently selection into the mutation xml tag.
During domToMutation I take the value from the saved mutation tag and use it to set an attribute on this called 'mutator_saved'.

Then the getOptions function is implemented such that if 'this.mutator_saved' is set, it returns that as the only option and deletes this.mutator_saved, otherwise it just lists the normal options. That way the selected option remains persistent if you serialize and then deserialize to xml.

The key thing here to realize is that the mutator object is mixed in with the original block object, such that they have the same attributes.

Right at the end of domToMutation I call getOptions and setValue on the dropdown field. I can't quite remember why I do this but I have a feeling it is to do with blockly caching the result of the getOptions function.

I'll see if I can tidy some of my code up and put it in a gist for you to look at.

Lewis

Beka Westberg

unread,
Jun 21, 2021, 9:39:20 AM6/21/21
to Blockly
Hi! Just wanted to note tha the finished loading event is definitely available! See docs here. I think the problem is that the issue just never got re-closed after my question hehe. Thank you for bringing it up so I could close it!

Best wishes,
--Beka

Bart Butenaers

unread,
Jun 21, 2021, 4:41:35 PM6/21/21
to Blockly
Hello Lewis,
Thanks for joining this discussion again!  I have experimented in all kind of ways with your advice, but no luck.  Indeed now the first time getOptions is being called, it returns the save options from this.mutator_saved.  But when I clear it then, the getOptions is again being called but now it returns an empty option list.
When I try your trick (to call getOptions in domToMutation) it becomes even worse, because then getOptions clears  this.mutator_saved  immediately after it has been set...  
It must be a ridiculous mistake somewhere, but I don't see it ;-( 
So if you could find some time to share some of your code, that would be very kind!

Hi Beka,
Thanks for the link to the documentation!  After I had seen the Github issue, I concluded this feature was not available yet (without further looking in the documentation)...

Bart Butenaers

unread,
Jun 22, 2021, 2:18:37 PM6/22/21
to Blockly
One step further.  Seems it was going wrong because I used  dropdownField.getOptions to get the options to store in the mutation:

    mutationToDom: function() {
        debugger;
        // Save the options of the NAME dropdown field, to make sure they are still available when the workspace is loaded.
        var mutationXml = document.createElement('mutation');
        var dropdownField = this.getField('NAME');
        var dropdownOptions = dropdownField.getOptions(false);
        var dropdownOptionsAsJson = JSON.stringify(dropdownOptions);
        mutationXml.setAttribute('dropdown_options', dropdownOptionsAsJson);
        return mutationXml;
    }

It was solved by getting the current available options by calling my function:

    mutationToDom: function() {
        debugger;
        // Save the options of the NAME dropdown field, to make sure they are still available when the workspace is loaded.
        var mutationXml = document.createElement('mutation');
        var dropdownOptions = getDropdownOptions(this);
        var dropdownOptionsAsJson = JSON.stringify(dropdownOptions);
        mutationXml.setAttribute('dropdown_options', dropdownOptionsAsJson);
        return mutationXml;
    }

I think I am almost at the end of this bumpy ride ....

Lewis Newman

unread,
Jun 22, 2021, 2:22:29 PM6/22/21
to Blockly
Forget to add this earlier, this is one of the examples I had come up with.

Lewis

Bart Butenaers

unread,
Jun 22, 2021, 6:01:55 PM6/22/21
to Blockly
Lewis,
You nailed it.  Indeed the setValue (immediately after the getOptions) seems to cache the options somewhere!
You are my hero for today ;-)
Bart

Bart Butenaers

unread,
Jun 23, 2021, 5:27:49 PM6/23/21
to Blockly
Struggling with an edge case.  Suppose my dropdown contains as options the values A/B/C from 3 other nodes N1/N2/N3. 
Suppose option A (from node N1) is currently selected in the dropdown.
But two things might happen with the node N1:
  1. Node N1 might be removed from the workspace
  2. The value A in node N1 might be changed to A'
In both cases the value A doesn't exist anymore in the workspace, so it cannot be the selected value in the dropdown anymore.
Which means both cases should somehow trigger another value to be selected in the dropdown.

Does anybody has any tips how to accomplish this?  Should I use a listener on the workspace or something else?µ
Thanks again!!

Beka Westberg

unread,
Jun 26, 2021, 10:19:30 AM6/26/21
to Blockly
I think you're definitely right that you should use a listener on the workspace. That's how I would approach it. You probably want to listen for block change events.

Best of luck!
--Beka

Lewis Newman

unread,
Jun 26, 2021, 11:02:25 AM6/26/21
to Blockly
Adding onto what Beka said.

The way I approached this problem exploit the fact that a dropdown option is made up of 2 values, the display-value and the raw-value. In my case I set the raw-value to be the id of the block that the dropdown is referring to and set the display value to the name of the block that is selected. I then did what Beka suggested where I have a listener on the workspace which then goes through all the blocks and double checks all the values. If a block name changed then I updated the display-value to reflect the fact that the block name has changed, and if a block has disappeared entirely then I reset the dropdown to a default null value.

Lewis

Bart Butenaers

unread,
Jul 3, 2021, 10:17:35 AM7/3/21
to Blockly
Hi Beka, Lewis,
Sorry for the delay, but few spare time lately...
This is VERY useful feedback.  I'm refactoring my code based on your advice, and will post my code here when finished.
Bart

Bart Butenaers

unread,
Jul 31, 2021, 7:05:15 PM7/31/21
to Blockly
Hi Beka, Lewis,

I had promised to come back here to share my solution once everything was working.  
Without your help I would have NEVER get this working.
Because some edge cases I had to redesign my design from scratch (see code here if anybody ever needs it).

Summarized my current design of timers works like this:
  1. My  clear_timeout and  clear_interval blocks have a dropdown, that needs to be filled with the names of  respectively all set_timeout and clear_timeout block names.
  2. Those dropdown values are (de)serialized using a  dropdownMutator.  So the mutator takes care of loading the dropdown options after the workspace is reloaded.
  3. When a  Blockly.Events.BLOCK_CREATE event occurs:
        a) if a  set_timeout block is created, then it's id/name pair is added as option in the clear_timeout dropdown.
        b) if a  set_interval block is created, then it's id/name pair is added as option in the clear_interval dropdown.
  4. When a  Blockly.Events.BLOCK_DELETE event occurs:
        a) if a  set_timeout block is deleted, then it's id is deleted as option from the clear_timeout dropdown.
        b) if a  set_interval block is deleted, then it's id is deleted as option in the clear_interval dropdown.
  5. When a  Blockly.Events.BLOCK_CHANGE event occurs:
        a) if the name field of a  set_timeout block is changed, then the name of that id option in the clear_timeout dropdown is changed.
        b) if the name field of a  set_interval block is changed, then the name of that id option in the clear_interval dropdown is changed.
By centralizing all the logic into the event handler, it was much easier to make it stable so all edge cases were covered.

Here is a short demo for those who are wondering what it looks like:

timer_demo_google.gif

For me this case is closed...

Thanks again!!!
Bart

Beka Westberg

unread,
Aug 2, 2021, 9:48:30 AM8/2/21
to Blockly
Thanks for sharing the code, and the demo! Handle cases like this where your blocks depend on arbitrary other blocks in the workspace is always so tricky. I'm glad you found something that could work for you =) It looks like a great solution!
Reply all
Reply to author
Forward
0 new messages