Hi Utkarsh,
Could you tell me what error you're getting? I had tested the code locally before posting, and it worked. I just forked the code sandbox from
https://groups.google.com/g/blockly/c/X4ZyWJF7tZU, adding (1) the code I posted, (2) code to set localStorage with appropriate test data, and (3) code to load two of these blocks from XML after about 5 seconds. (I also added a const before fieldToggleMixin to declare the variable so you don't get an error if running in strict mode as well as an empty JavaScript language generator for the block.)
To address your questions about the changes I made and make some suggestions, I'll lead off by explaining how the options for a dropdown menu are determined:
When you pass a reference to a function as the first argument to the Blockly.FieldDropdown constructor as you do here, this function will become the menu generator that getOptions calls to determine the list of options. getOptions is called when the dropdown is being created, when the user clicks on that dropdown, and when you call it directly. Each time getOptions is called, the menu generator function you supplied is called to get its return value, which is then stored and used as the (possibly new) list of options. When the dropdown is being created, it is initialized having the first option selected (
https://github.com/google/blockly/blob/master/core/field_dropdown.js#L84).
So now let's see what goes wrong and why calling getOptions on the two column fields after setting the table field fixes this. First upon deserialization from XML:
Say the first referenced table in the dropdown has columns A, B, C, and D and the second table has columns E, F, and G. When the table field is created, it does so with the first option selected by default. So when the menu generator passed to the column_input_field dropdown constructor is first called, it returns the columns from this first table, mainly A, B, C, and D with A selected (the first option). Then when the menu generator for column_input_field1 is first called, it will return the list of columns from the first table excluding the selected A of the second dropdown, mainly the list of B, C, and D.
But suppose the XML specified that the selected values for table_input_field, column_input_field, and column_input1_field should be the second table, G, and E, respectively, instead. The second table is a valid option so there's no problem setting the table. But after doing that, it does not regenerate the options for the column input fields. So when it tries to set column_input_field to G, it says that's not possible because my only available options are A, B, C, and D. Similarly, when trying to set column_input_field1 to E, it fails saying my only options are B, C, and D.
However, you can fix this by writing code in domToMutation, which will be called before the computer sets the values of fields as specified in the XML. So we call this.setFieldValue to set the table_input_field to the tableId specified by the mutation. This by itself would be unnecessary as the table dropdown would automatically be set to the correct value from the XML. But by then calling getOptions on column_input_field and column_input_field after setting the table, you are forcing their corresponding menu generators to be called again when the selected option is the second table. This means that the menu generator for column_input_field will instead return the list of options of columns from the second table, mainly E, F, and G. The options for column_input1_field will also be E, F, and G. So now the field values specified in the XML can be successfully set. As before, table is set to the second one, which does nothing since it was already set in domToMutation. But this time the selected options of G for column_input_field and E for column_input1_field are both valid so they go through.
Now to address the change listener. When reading that column_input1_field will also be E, F, and G, you may have wondered why column E was not excluded. That's because the original selected option of A was not changed by calling getOptions, even if it's no longer a valid option. So when the values for column_input1_field are generated, it only checks that E, F, and G are not A, which they of course are not.
Similarly, using this example from before, if you click the table dropdown and change the selected table from table 2 back to table 1, the selected option for column_input_field remains G. This is true even though the menu generator now returns the list of columns from the selected first table of A, B, C, and D. To fix this issue, I had the onchange listener first call getOptions for column_input_field to see if the selected option was present. If not, it would set it to the first available one. So in this case fieldOptions would be set to [["Column A", "a"], ["Column B", "b"], ["Column C", "c"], ["Column D", "d"]], the return value from getOptions called on the field column_input. Calling map(opt => opt[1]) on this results in ["a", "b", "c", "d"], which it then checks to see if it includes "g". Since it doesn't, it selects fieldOptions[0][1], the first option value of "a" and sets it as the new field value for column_input_field. Next, it calls getOptions for column_input1_field, which now results in ["Column B", "b"], ["Column C", "c"], ["Column D", "d"]]. Since e is not in this list, it sets it to the first available one of b.
So finally, to address changes I made and how you could shorten your code:
1. As alluded to, you don't need to call the variable options in the menu generator function you use as the menu is only determined by the return value and is unaffected by the choice of local variable names. So for the get_tables extension, for example, you can easily use a one-line arrow function definition (without braces or the use of the return keyword):
() =>
JSON.parse(localStorage.getItem("applab_myTables")) // tables
The same applies to the other two extensions, which I had modified accordingly.
Also as a few stylistic notes if you were to stick with what you had, you could use const options and const table instead of let options and let table. For table, the use of const may seem a little nonintuitive as it initially refers to an empty array before adding values to it, so it doesn't seem to be constant. However, in JavaScript, const just declares that you're not changing the reference to the object that the variable stores; the object's (Array's) properties can still be changed. E.g., you always refer to the same car even though the car's miles traveled, fuel, etc. can change. In general, it's better to declare variables with const when you don't expect their values to change as a way of letting readers know this and as a way of preventing accidental modifications. Also, I'd avoid using an instruction that changes state (push called on options) in map as map is generally only used to create a new Array from an existing one. Since the function you're passing is used only for its side effects, mainly appending to options, and you're only using map to repeatedly call it for each table in the Array, I'd use forEach instead.
2. filter(cId => table['columnOrder'].includes(cId)).filter(cId => cId !== selectedColumn) could be replaced with filter(cId => table['columnOrder'].includes(cId) && cId !== selectedColumn) as I had changed. This creates a new Array in which both conditions were simultaneously met instead of first creating a new subarray of the items meeting the first condition of table['columnOrder'].includes(cId) and then creating a new subarray from the resulting one in which the second condition cId !== selectedColumn was met.
3. Removing and adding fields is also unnecessary. As mentioned earlier, you can call getOptions to regenerate them in response to changes in the menu options that are selected and use the onchange listener to simply change the selected options when they're no longer in the lists of valid ones.
4. If you went with your current implementation, you could remove the repetition of the anonymous functions by giving them a name and then passing a reference to it:
E.g.:
function columnInputFieldGenerator() {
let options = []
let table = JSON.parse(localStorage.getItem('applab_myTables')).find(table => table.id === event.newValue) Object.keys(table['columnData']).filter(cId => table['columnOrder'].includes(cId)).map(cId => options.push([table['columnData'][cId]['name'], cId]))
return options
}
In that case, you could replace the occurrences of:
this.getInput('column_input')
.appendField(new Blockly.FieldDropdown(
function () {
let options = []
let table = JSON.parse(localStorage.getItem('applab_myTables')).find(table => table.id === xmlElement.getAttribute('tableId')) Object.keys(table['columnData']).filter(cId => table['columnOrder'].includes(cId)).map(cId => options.push([table['columnData'][cId]['name'], cId]))
return options
}
), 'column_input_field')
with this.getInput('column_input')
.appendField(new Blockly.FieldDropdown(columnInputFieldGenerator), 'column_input_field')
There is more refactoring that you could do to condense your code even more, but given that this response is already too long, I'll hold off on any further suggestions for now.
Hopefully this makes sense. Feel free to respond with any additional questions.
Best,
Jason