Problem with Dynamic Dropdowns

168 views
Skip to first unread message

Jason Morris

unread,
Feb 9, 2023, 1:25:44 PM2/9/23
to Blockly
I have a block called a "category declaration". Within a single project there will be many workspaces, and category declarations can exist in any number of workspaces.

I have a block called "attribute declaration" that has a dropdown that is supposed to allow the user to select one of the categories that has already been declared.

The JS code for that block is as follows:

Blockly.Blocks['new_attribute_declaration'] = {
  init: function() {
    this.appendDummyInput()
        .appendField("The category")
        .appendField(new Blockly.FieldDropdown(this.generateCategories(),"category_name"))
        .appendField("has an attribute")
        .appendField(new Blockly.FieldTextInput("attribute name"), "attribute_name");
    this.appendDummyInput()
        .appendField("which is of type")
        .appendField(new Blockly.FieldDropdown(this.generateDataTypes(), "attribute_type"))
        .appendField(", appearing as")
        .appendField(new Blockly.FieldDropdown([["object, then value","ov"], ["value, then object","vo"]]), "order");
    this.appendDummyInput()
        .appendField(new Blockly.FieldImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAn0lEQVQI1z3OMa5BURSF4f/cQhAKjUQhuQmFNwGJEUi0RKN5rU7FHKhpjEH3TEMtkdBSCY1EIv8r7nFX9e29V7EBAOvu7RPjwmWGH/VuF8CyN9/OAdvqIXYLvtRaNjx9mMTDyo+NjAN1HNcl9ZQ5oQMM3dgDUqDo1l8DzvwmtZN7mnD+PkmLa+4mhrxVA9fRowBWmVBhFy5gYEjKMfz9AylsaRRgGzvZAAAAAElFTkSuQmCC", 15, 15, { alt: "\"", flipRtl: "FALSE" }))
        .appendField(new Blockly.FieldTextInput(""), "prefix")
        .appendField(new Blockly.FieldLabelSerializable("object"), "first_element")
        .appendField(new Blockly.FieldTextInput("'s attribute name is"), "infix")
        .appendField(new Blockly.FieldLabelSerializable("value"), "second_element")
        .appendField(new Blockly.FieldTextInput(""), "postfix")
        .appendField(new Blockly.FieldImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAKCAQAAAAqJXdxAAAAqUlEQVQI1z3KvUpCcRiA8ef9E4JNHhI0aFEacm1o0BsI0Slx8wa8gLauoDnoBhq7DcfWhggONDmJJgqCPA7neJ7p934EOOKOnM8Q7PDElo/4x4lFb2DmuUjcUzS3URnGib9qaPNbuXvBO3sGPHJDRG6fGVdMSeWDP2q99FQdFrz26Gu5Tq7dFMzUvbXy8KXeAj57cOklgA+u1B5AoslLtGIHQMaCVnwDnADZIFIrXsoXrgAAAABJRU5ErkJggg==", 15, 15, { alt: "\"", flipRtl: "FALSE" }));
    this.setInputsInline(false);
    this.setPreviousStatement(true, ["OUTER", "STATEMENT"]);
    this.setNextStatement(true, "STATEMENT");
    this.setColour(45);
 this.setTooltip("Use to create an attribute for a category.");
 this.setHelpUrl("/docs/blocks/new_attribute/");
  },
  generateCategories: function() {
    var all_workspaces = getAllWorkspaces();
    // console.log("All Workspaces:")
    // console.log(all_workspaces)
    var knownCategoriesList = [];
    for (var w = 0; w < all_workspaces.length; w++) {
        // Go through the blocks in the workspace.
        // If the block is an object declaration, add the relevant block to the xml
        if (all_workspaces[w].xml_content) {
            var domObject = Blockly.Xml.textToDom(all_workspaces[w].xml_content);
            var tempWorkspace = new Blockly.Workspace();
            Blockly.Xml.domToWorkspace(domObject, tempWorkspace);
            var blockList = tempWorkspace.getAllBlocks();
            // console.log("BlockList: " + blockList)
            for (var i = 0; i< blockList.length; i++) {
                if (blockList[i].type == "category_declaration") {
                    // Get the name of the entity, insert a block of that type,
                    var category_name = blockList[i].getFieldValue('category_name');
                    knownCategoriesList.push([category_name,category_name]);
                }
            }
            delete tempWorkspace;
        }
    }
    for (var id in importDictionary) {
      var blockList = importDictionary[id].getAllBlocks();
      for (var i = 0; i< blockList.length; i++) {
          if (blockList[i].type == "category_declaration") {
          // Get the name of the entity, insert a block of that type,
          var category_name = blockList[i].getFieldValue('category_name');
          knownCategoriesList.push([category_name,category_name]);
          }
      }
    }
   
    return knownCategoriesList;
  },
  generateDataTypes: function() {
    var options = [["true / false","boolean"], ["number","number"], ["date","date"], ["time","time"], ["datetime","datetime"], ["duration","duration"]]
    var knownCategories = this.generateCategories();
    for (var i=0; i < knownCategories.length; i++){
      options.push(knownCategories[i]);
    }
    return options;
  }
};

This code works for generating the block in the toolbox. And if you drag it into the workspace, it appears in the workspace, and the options have been correctly generated. But as soon as it appears it is locked in it's initial position, and it immediately generates an error, which recurs if you try to drag the block in the workspace:

Uncaught TypeError: b.call is not a function
    at FieldDropdown$$module$build$src$core$field_dropdown.setValue (field.ts:978:39)
    at InsertionMarkerManager$$module$build$src$core$insertion_marker_manager.createMarkerBlock (insertion_marker_manager.ts:275:23)
    at new InsertionMarkerManager$$module$build$src$core$insertion_marker_manager (insertion_marker_manager.ts:135:29)
    at new BlockDragger$$module$build$src$core$block_dragger (block_dragger.ts:65:9)
    at Gesture$$module$build$src$core$gesture.startDraggingBlock_ (gesture.ts:417:9)
    at Gesture$$module$build$src$core$gesture.updateIsDraggingBlock_ (gesture.ts:353:12)
    at Gesture$$module$build$src$core$gesture.updateIsDragging_ (gesture.ts:404:14)
    at Gesture$$module$build$src$core$gesture.updateFromEvent_ (gesture.ts:243:12)
    at Gesture$$module$build$src$core$gesture.handleMove (gesture.ts:557:12)
    at HTMLDocument.g (browser_events.ts:78:9)

I don't know what to make of that. From googling it seems like it might be a sync/async problem, but I'm not sure where to look to find out.

Once the block has been added once, if I try to open any dynamically-generated drawer of the toolbox after that, I get an infinite loop error.

xml.ts:906 Uncaught RangeError: Maximum call stack size exceeded
    at domToBlockHeadless$$module$build$src$core$xml (xml.ts:906:28)
    at domToBlock$$module$build$src$core$xml (xml.ts:577:16)
    at Object.domToWorkspace$$module$build$src$core$xml [as domToWorkspace] (xml.ts:457:23)
    at Block$$module$build$src$core$block.generateCategories (blawx-blocks.js:2969:25)
    at Block$$module$build$src$core$block.init (blawx-blocks.js:2935:53)
    at Block$$module$build$src$core$block.doInit_ (block.ts:295:14)
    at new Block$$module$build$src$core$block (block.ts:276:12)
    at Workspace$$module$build$src$core$workspace.newBlock (blockly.ts:547:14)
    at domToBlockHeadless$$module$build$src$core$xml (xml.ts:917:21)
    at domToBlock$$module$build$src$core$xml (xml.ts:577:16)

If I add a category declaration block that should show up in the dropdown options, it does not.

Even stranger, this infinite loop problem seems to keep happening if I delete all of the blocks, exit the page, and re-open it.

I'd appreciate any advice on what I'm doing wrong, or where I could look to figure it out.

Jason

Jason Morris

unread,
Feb 9, 2023, 3:10:00 PM2/9/23
to Blockly
The problem with field.ts returning "TypeError: b.call is not a function" can evidently be resolved by explicitly adding the opt_validator to the FieldDropdown constructor:

e.g.  .appendField(new Blockly.FieldDropdown(this.generateDataTypes(), opt_validator=function(n) {return n;}, "attribute_type"))

I also found the cause of the infinite loop. Finding category declarations requires loading the XML for the current and other workspaces in as a domToWorkspace. That, in turn, causes these blocks to be loaded, which causes them to be initialized, which causes them to repeat the search.

It's a bit of a kludge, and I'd be grateful for a more graceful approach, but what I'm doing for now is setting a global variable called "headless" to true at the top of the generateCategories function, setting it to false at the bottom, and having it return a static optionList if it is true when the function is called.

Someone who understands the context better will have to check, but it feels to me like field.ts is calling a validator that doesn't exist, which seems like it might actually be a bug. Validators are supposed to be optional, and here Blockly crashes in the absence of one.

Maribeth Bottorff

unread,
Feb 9, 2023, 8:49:20 PM2/9/23
to Blockly
Hello,

I have a couple of thoughts about your code.


Regarding refreshing the list & the problem with the optional validator: you want to change the following line of code
      .appendField(new Blockly.FieldDropdown(this.generateCategories(),"category_name"))

to 
      .appendField(new Blockly.FieldDropdown(this.generateCategories),"category_name")

Note the difference in parentheses in two places:

1. You are actually not using a dynamic dropdown right now. You are simply calling the `generateCategories` function once, immediately at block creation time, and the resulting list is passed into the field constructor. Instead, you need to pass the function in (not call it). without the parentheses after `generateCategories`, you are now passing in this function to Blockly, and we will call it every time your dropdown is opened.
2. "category_name" should be the second parameter passed to appendField. In your code, you are passing it as the second parameter to the FieldDropdown constructor instead, where it is being interpreted as the validator function. With the parentheses in the correct place, the validator is indeed optional, as if you do not pass an argument Blockly will not try to call it.

Regarding your infinite loop:
Why do you need to create a new workspace and copy the blocks onto it? Are these workspaces on the current page already? If not, could you create them (perhaps as headless workspaces) once instead of every time you open the dropdown? If so, then you could just check the blocks on the workspace directly, e.g.

generateCategories: function() {
    var all_workspaces = Blockly.common.getAllWorkspaces();
    var knownCategoriesList = [];
    for (var w = 0; w < all_workspaces.length; w++) {
        // Go through the blocks in the workspace.
        // If the block is a category declaration, add the category to the list
            var blockList = w.getAllBlocks();
            // console.log("BlockList: " + blockList)
            for (var i = 0; i< blockList.length; i++) {
                if (blockList[i].type == "category_declaration") {
                    var category_name = blockList[i].getFieldValue('category_name');
                    knownCategoriesList.push([category_name, category_name]);
                }
        }
    }
    // Rest of your function omitted for clarity
   
    return knownCategoriesList;
  },

I hope this helps! Please let us know if you have additional questions.

Maribeth

Jason Morris

unread,
Feb 13, 2023, 6:04:48 PM2/13/23
to Blockly
Thank you, Maribeth:

I had found the same problem in another dropdown, but hadn't noticed that I had repeated the mistake elsewhere!

That has completely solved the problem for me. And yeah, I'm aware there are a LOT of inefficiencies in the code, among them the one you identified. I'm just trying to get a working prototype and then I'll refactor some of that stuff away.

J
Reply all
Reply to author
Forward
0 new messages