How to add custom blocks to the workspace with javascript functions

362 views
Skip to first unread message

Natalie Yang

unread,
Jun 30, 2021, 11:07:10 AM6/30/21
to Blockly
Hello,
I want to add blocks to the workspace with some functions rather than drag blocks from the toolbox.
Like, When I enter "a" in a text area and press button one, a block will add to the workspace like:
螢幕快照 2021-06-30 下午10.16.41.png
And when I enter "5" in another text area and press button two, a block will add like: :

螢幕快照 2021-06-30 下午10.57.18.png
How can I do this?
I would like to know how to set variables (like "a" above), values(like "5" above) and connect them with javascript functions.

Thank you very much!!

Jason Schanker

unread,
Jun 30, 2021, 2:33:18 PM6/30/21
to Blockly
Hi,

To create a new variables_set block, you could use:

const workspace = Blockly.getMainWorkspace();
const variableSetBlock = workspace.newBlock('variables_set');
variableSetBlock.initSvg();

To set a field value on a block you can use block.setFieldValue(value, fieldName) but the values of a FieldVariable dropdown are ids that are randomly generated so you need to first get the id of the variable name you want to set it to first.  To do this (assuming some textarea with id variable_name_textarea held the variable name string to use), you could do the following:

variableModel = workspace.getVariable(document.getElementById('variable_name_textarea').value);
if(variableModel) { // does variable with this name exist?
  variableId = variableModel.getId();
  variableSetBlock.setFieldValue(variableId, 'VAR');
}

Similarly, you could create a number block and set its field value this way (without any lookup):

const mathNumberBlock = workspace.newBlock('math_number');
mathNumberBlock.setFieldValue(document.getElementById('number_textarea').value, 'NUM');
mathNumberBlock.initSvg();

To connect these blocks, you would connect the VALUE input connection of the variables_set block to the output connection of the number block (or vice versa) as follows:

variableSetBlock.getInput('VALUE').connection.connect(mathNumberBlock.outputConnection);

Finally to see these changes to the workspace, you can render it as follows:

workspace.render();

(The links are to the relevant code in the Blockly repository.)

Best,
Jason

Natalie Yang

unread,
Jul 1, 2021, 2:07:16 AM7/1/21
to Blockly
Hello,
I still have some problems generating the variable and the id.
First,
I follow the steps, but my blocks look like this:
螢幕快照 2021-07-01 下午1.37.43.png
I have a textarea in my index.html file like this:
<textarea id="variable_name_textarea" name="variable_name_textarea"> abc </textarea>

code in my main.js like this:
variableModel = workspace.getVariable(document.getElementById('variable_name_textarea').value);
if(variableModel) { // does variable with this name exist?
  variableId = variableModel.getId();
  variableSetBlock.setFieldValue(variableId, 'VAR');
}

But console.log(variableModel) shows null !
So I can't set the variable name (like "abc" ) successfully.
How can I fix it?

Second,
The code of the blocks above looks like: 
"item = None item = 5"
which generates from Blockly.Python.workspaceToCode(workspace);
How can I make it looks like:
"item = 5;"

Last,
Can I just set the variable name from a javascript array without getting elements from the html file?

For example:
If I have a javscript array like:
var names=["a","b","c"];

Can I generate blocks like the picture below?
螢幕快照 2021-07-01 下午1.57.08.png

Thank you very much!!!

Best,
Natalie
jason.s...@gmail.com 在 2021年7月1日 星期四上午2:33:18 [UTC+8] 的信中寫道:

Jason Schanker

unread,
Jul 1, 2021, 4:05:38 AM7/1/21
to Blockly
Hi Natalie,

(1) When no variable with that name exists, getVariable will return null so the body of the if statement which sets the variable block won't be run.  If you want the user to be able to create new variables as well, you can modify what I wrote as follows that will create a new variable with that name if it doesn't exist:

const variableName = document.getElementById('variable_name_textarea').value;
const variableModel = workspace.getVariable(variableName);
const variableId = (variableModel || workspace.createVariable(variableName)).getId();
variableSetBlock.setFieldValue(variableId, 'VAR');


(3) Yes, you can loop through all of the elements in the Array to do this, essentially using the code from before in the loop body, adding a statement to connect previous/next connections (and setting the variableName to the Array elements instead).

const workspace = Blockly.getMainWorkspace();
const variableNames = ["a", "b", "c"];
const variableSetBlocks = [];

for(let i = 0; i < variableNames.length; i++) {
  const variableModel = workspace.getVariable(variableNames[i]);
  const variableId = (variableModel || workspace.createVariable(variableNames[i])).getId();
  const mathNumberBlock = workspace.newBlock('math_number');

  variableSetBlocks.push(workspace.newBlock('variables_set'));
  variableSetBlocks[i].initSvg();
  variableSetBlocks[i].setFieldValue(variableId, 'VAR');
  if(i > 0) {
    variableSetBlocks[i].previousConnection.connect(variableSetBlocks[i-1].nextConnection);
  }

  mathNumberBlock.setFieldValue(document.getElementById('number_textarea').value, 'NUM');
  mathNumberBlock.initSvg();
  variableSetBlocks[i].getInput('VALUE').connection.connect(mathNumberBlock.outputConnection);
}

workspace.render();

Best,
Jason

Natalie Yang

unread,
Jul 4, 2021, 3:24:59 AM7/4/21
to Blockly
Hi,
Thank you very much! The answers helped a lot!

I still have a problem with generating code.
Here are the blocks that I created:
螢幕快照 2021-07-04 下午2.49.26.png
The code generated from the blocks looks like:
a = None i = None def upRange(start, stop, step): while start <= stop: yield start start += abs(step) def downRange(start, stop, step): while start >= stop: yield start start -= abs(step) a = 9 for i in (1 <= float(a)) and upRange(1, float(a), 2) or downRange(1, float(a), 2): print(i)

I found that might because I have a variable_get type block as input.
How can I make it looks like:
a = 9
for i in range(1, a, 2): print(i)

Or I have to build a custom generator by myself?

Best,
Natalie
jason.s...@gmail.com 在 2021年7月1日 星期四下午4:05:38 [UTC+8] 的信中寫道:

Jason Schanker

unread,
Jul 5, 2021, 12:14:21 AM7/5/21
to Blockly
Well, if you don't mind having a slightly hacky solution and you don't have any function definitions, the easiest way would be to override Blockly.Python.finish, the function that adds the variable/function definitions to the beginning of your code as follows:

Blockly.Python.finish = function(code) {
  // Convert the definitions dictionary into a list.
  var imports = [];
  var definitions = [];
  for (var name in this.definitions_) {
    var def = this.definitions_[name];
    if (def.match(/^(from\s+\S+\s+)?import\s+\S+/)) {
      imports.push(def);
    } else {
      definitions.push(def);
    }
  }
  // Call Blockly.Generator's finish.
  code = Object.getPrototypeOf(this).finish.call(this, code);
  this.isInitialized = false;

  this.nameDB_.reset();
  // var allDefs = imports.join('\n') + '\n\n' + definitions.join('\n\n');
  // return allDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n\n') + code;
  return code;
};

You will also need to override the Python generator for any blocks you use that adds guards to protect against errors.  For example, for the for loop, you could use the following to override the Python generator for the controls_for block:

Blockly.Python['controls_for'] = function(block) {
  // For loop.
  var variable0 = Blockly.Python.nameDB_.getName(
      block.getFieldValue('VAR'), Blockly.VARIABLE_CATEGORY_NAME);
  var argument0 = Blockly.Python.valueToCode(block, 'FROM',
      Blockly.Python.ORDER_NONE) || '0';
  var argument1 = Blockly.Python.valueToCode(block, 'TO',
      Blockly.Python.ORDER_NONE) || '0';
  var increment = Blockly.Python.valueToCode(block, 'BY',
      Blockly.Python.ORDER_NONE) || '1';
  var branch = Blockly.Python.statementToCode(block, 'DO');

  return 'for ' + variable0 + ' in range(' + argument0 + ', ' + argument1 + ', ' + increment + '):\n' + branch;
}

Best,
Jason

Beka Westberg

unread,
Jul 5, 2021, 10:19:50 AM7/5/21
to Blockly
Hello,

Looks like Jason has got you covered here =) But I just wanted to say, with regard to removing the initial variable declarations, you might want to read through this discussion first to make sure that you're not going to run into any edge cases.

Best wishes,
--Beka

Jason Schanker

unread,
Jul 6, 2021, 12:36:03 AM7/6/21
to Blockly
And speaking of cases where there will be issues with code generation, commenting out the lines I did will also mean that import statements won't be included, which means for example that referencing certain functions/constants such as math.sqrt, random.random, and math.pi will all cause errors.  So to fix this, you could prefix the code with imports but not definitions:


Blockly.Python.finish = function(code) {
  // Convert the definitions dictionary into a list.
  var imports = [];
  var definitions = [];
  for (var name in this.definitions_) {
    var def = this.definitions_[name];
    if (def.match(/^(from\s+\S+\s+)?import\s+\S+/)) {
      imports.push(def);
    } else {
      definitions.push(def);
    }
  }
  // Call Blockly.Generator's finish.
  code = Object.getPrototypeOf(this).finish.call(this, code);
  this.isInitialized = false;

  this.nameDB_.reset();
  var importDefs = imports.join('\n');
  return importDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n\n') + code;
};

Also, it is still possible to define your own functions but remove added variable initializations if you modify the init function instead of the finish one as Maribeth had mentioned in the discussion Beka referenced.

Best,
Jason
Reply all
Reply to author
Forward
0 new messages