How to initialize a block with a certain number of inputs

515 views
Skip to first unread message

Max Koretskyi

unread,
Jul 19, 2023, 6:15:00 AM7/19/23
to Blockly
I need to have blocks that have a varible number of inputs. For the dictionary like this:

const fns = {
  abs: 1,
  add: 2,
  ...
}

I could define a different type of block for each type of fns. Instead, what I want to do is to define a generic block with no inputs and add the required number of inputs during initialization. I almost made it work like this:

export const call_expression_json = {
  type: 'kw-call_expression',
  message0: '',
  args0: [],
  extensions: ['add_args']
};

Blockly.Extensions.registerMixin('add_args ', {
  addArgs() {
for (let i=0; i<this.data.argsNumber; i++) {
  this.appendValueInput('input' + i);
}
  },
}

and now when I create the block like this:

block = this.workspace.newBlock('kw-call_expression');
block.data = {argsNumber: 3};
block.addArgs();

it's rendered in the workspace.

However, the problem is when I move the block, I get the error:

> ERROR Error: The insertion marker manager tried to create a marker but the result is missing an input. If you are using a mutator, make sure your domToMutation method is properly defined.

which I assume comes from the fact that when a block is moved it's cloned (de-serialized/serialized). When this happens, in my case, Blockly can't find the corresponding input in the cloned block, as it hasn't been added yet.

So as I understand, I'm forced to use serialization hooks, but I'd want to avoid that as it adds complexity.

The other option is to create inputs in the initialization phase, e.g. inside mixin function, but at this point I don't yet have `data` property added.

It'd be great to have a way to pass initiazation params to the block when creating it:

block = this.workspace.newBlock('kw-call_expression_T', {argsCount: 3});

any suggestions?

Max Koretskyi

unread,
Jul 19, 2023, 11:46:41 AM7/19/23
to Blockly
So the implementation that uses a mutator will look like this:

Blockly.Extensions.registerMutator('add_args_Mutator', {
addArgs() {
for (let i = 0; i < this.data.argsNumber; i++) {

this.appendValueInput('input' + i);
}
},
saveExtraState: function () {
return {
argsNumber: this.data?.argsNumber,
};
},

loadExtraState: function (state) {
this.argsNumber = state['argsNumber'];
for (let i = 0; i < this.argsNumber; i++) {

this.appendValueInput('input' + i);
}
},
});

and the calling is like this:

block = this.workspace.newBlock('kw-call_expression_T');

block.data = {argsNumber: 3};
block.addArgs();

but I'd like to avoid using the mutator somehow.

Maribeth Bottorff

unread,
Jul 19, 2023, 8:10:39 PM7/19/23
to Blockly
Hi Max,

If you are going to change the shape of your block after initialization, you do need to use the mutator for serialization hooks. As you've discovered, Blockly uses serialization for many different things internally, plus you want to be able to save your users' code correctly. If your block's true state doesn't match the result of running the `init` method, then Blockly has no other mechanism for determining your block's shape other than the mutator.

Your idea for passing in data as a parameter is interesting, but you don't have control over all the places `newBlock` would be called. For example, Blockly might have to call `newBlock` when a user drags the block from the toolbox. How would Blockly know what parameters to pass? In fact, the mutator solves this problem, by providing that extra data that is needed in order to create the new block.

I hope that helps explain why the mutator is necessary. If you really want to avoid it, you'd have to make blocks that do not have a dynamic number of inputs.

Maribeth

Max Koretskyi

unread,
Jul 20, 2023, 10:49:18 AM7/20/23
to Blockly
Hey Maribeth, thanks for confirming that this is indeed the only proper way. 

One thing that confuses me is that with such implementation there's a duplication of state - `block.data` and `extraState`. They also available in different stages. I can't set `extraState` from outside when creating a block, so I need to define the extra state on the `block.data` property. However, the data property is not yet available in the loadExtraState when the block is being deserialized, so now I need to return the same data in the saveExtraState. What am I missing?

Max Koretskyi

unread,
Jul 20, 2023, 10:54:27 AM7/20/23
to Blockly
The implementation is like this:

const columnMutator = {
saveExtraState: function (this: any) {
return this.data;
},

loadExtraState: function (this: any, state) {
this.updateShape(state.value);
},

updateShape(this: any, fnName) {
this.appendDummyInput('EMPTY').appendField(fnName);
},
}

And the block is created like this:

block = this.workspace.newBlock('kw-column');
block.data = {...item};
block.updateShape(item.value);

Christopher Allen

unread,
Jul 20, 2023, 3:25:00 PM7/20/23
to blo...@googlegroups.com
Hi Max,

On Thu, 20 Jul 2023 at 16:49, Max Koretskyi <m.kor...@gmail.com> wrote:
One thing that confuses me is that with such implementation there's a duplication of state - `block.data` and `extraState`. They also available in different stages. I can't set `extraState` from outside when creating a block, so I need to define the extra state on the `block.data` property. However, the data property is not yet available in the loadExtraState when the block is being deserialized, so now I need to return the same data in the saveExtraState. What am I missing?

I agree that it seems wrong to have to store the same information twice.  Unfortunately I know of certain cases where this is unavoidable (for example, if you have two dropdown fields where the available choices in the second field are controlled by the value selected in the first field), but I don't think that your case is one of them.

Having a look at the block serialisation implementation, I observe:
So though it's not possible to completely avoid saving some extra "state", it does appear to be possible to avoid saving any duplicate information.


Christopher

Max Koretskyi

unread,
Jul 21, 2023, 7:54:35 AM7/21/23
to Blockly
thanks Chris, I tried what you suggested:

> A block's .loadExtraState method can therefore usefully access this.data, and could ignore its extraState argument entirely.

but the `data` is  null  in the  loadExtraState . Here the test setup, the error occurs when I start dragging the block:

const callExpressionMutator = {
saveExtraState: function (this: any) {
return {};

},

loadExtraState: function (this: any, state) {
console.log(this.data === null); // true
},
}

block = this.workspace.newBlock('kw-call_expression');
block.data = {...item};
block.updateShape(); 

Christopher Allen

unread,
Jul 21, 2023, 1:39:48 PM7/21/23
to blo...@googlegroups.com
Hi Max,

thanks Chris, I tried what you suggested…
but the `data` is  null  in the  loadExtraState.

That's curious.  I am sorry that I gave bad advice!
 
Here the test setup, the error occurs when I start dragging the block…

I was puzzled about why you would be getting errors when dragging the block.  Dragging from toolbox to workspace does use serialisation to copy the block, but I was puzzled why serialisation would be involved in an ordinary drag within the workspace—until Beka reminded me about insertion markers.

It turns out that createInsertionMarker uses saveExtraState / loadExtraState directly without using the normal Blockly.serialization code.  This discovery—thanks to your inquiry—has now sparked an internal discussion whether we can/should fix that.

In the meantime: loadExtraSate / saveExtraState are the canonical (and in this case required) way to serialise information that is used to determine the shape of a block.  If you want to avoid storing data redundantly, you could consider using it exclusively, instead of using .data, so your block-creation code would look like:

block = this.workspace.newBlock('kw-call_expression');
block.loadExtraState({...item});  // Calls block.updateShape() as required

(Implementation of saveExtraState / loadExtraState left as an exercise to the user.)


Best wishes,

Christopher

Max Koretskyi

unread,
Jul 24, 2023, 2:58:25 AM7/24/23
to Blockly
Thanks Chris for the feedback! I'll still need to store some information in a custom property, e.g ._data, so that I can return it in the  saveExtraState.  For now my code looks like this:

const columnMutator = {
_data: null,

setData(data) {
this._data = data;
this.updateShape();
},

saveExtraState: function (this: any) {
return this._data;

},
loadExtraState: function (this: any, state) {
this._data = state;
this.updateShape();
},
updateShape(this: any) {
const name = this._data.value;
this.appendDummyInput().appendField(name);
},
};

block = this.workspace.newBlock('kw-column');
block.setData({ ...item });

When I postprocess the serialized JSON version I also switch from reading extra information from ._data to reading it from extraState:

function columnToExpressionValue(block) {
const name = block.extraState.value;
... }

Christopher Allen

unread,
Jul 24, 2023, 8:57:32 AM7/24/23
to blo...@googlegroups.com
Hi Max,

Following up on my earlier message:

On Fri, 21 Jul 2023 at 19:39, Christopher Allen <cpca...@google.com> wrote:
It turns out that createInsertionMarker uses saveExtraState / loadExtraState directly without using the normal Blockly.serialization code.  This discovery—thanks to your inquiry—has now sparked an internal discussion whether we can/should fix that.


 On Mon, 24 Jul 2023 at 08:58, Max Koretskyi <m.kor...@gmail.com> wrote:
Thanks Chris for the feedback! I'll still need to store some information in a custom property, e.g ._data, so that I can return it in the  saveExtraState.

Indeed.  Not ideal, but perhaps better than having duplicate data in the serialisation.  Glad you seem to have gotten everything working  satisfactorily.


Best wishes,

Christopher

Max Koretskyi

unread,
Jul 25, 2023, 2:56:14 AM7/25/23
to Blockly
Great, thanks Chris!

Is my assumption correct that once 7316 is resolved, I'll be able to use the standardized .data property instead of the custom  ._data property?

Devansh Varshney

unread,
Aug 9, 2023, 4:07:35 PM8/9/23
to Blockly
Hi Max,

The issue 7316 is now resolved.

Regards

Beka Westberg

unread,
Aug 9, 2023, 4:46:18 PM8/9/23
to blo...@googlegroups.com
The fix should go out with release v10.1 this week =)

Best,
--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/01deea27-228e-40ad-98a5-eda295f8702cn%40googlegroups.com.

Max Koretskyi

unread,
Aug 10, 2023, 2:03:57 AM8/10/23
to Blockly
Thanks for letting me know!
Reply all
Reply to author
Forward
0 new messages