Mutator with automatically inserted "default" blocks

62 views
Skip to first unread message

Taurus Rico

unread,
Jul 14, 2026, 5:27:17 AMJul 14
to Blockly
Hello everyone,

I am trying to make an or-block with a mutator. In the mutation I want to place blocks automatically into empty inputs. These inserted blocks shouldn't be shadowblocks because the user should insert own blocks into the inputs of these blocks or replace the blocks entirely. My default block is a compare block named "inlineIfCompare".

My block definition and the mutator looks like that:
import * as Blockly from 'blockly/core';
import { MyCustomLabelField } from '../../custom_fields/labelFieldTest';
import '../../code_generators/orBlockCode';

Blockly.defineBlocksWithJsonArray([
{
type: 'or_block',
tooltip: 'verodern',
helpUrl: '',
message0: '%1 %2 %3 %4',
args0: [
{
type: 'input_value',
name: 'OR1INP',
check: 'Boolean',
align: 'RIGHT',
},
{
type: 'input_end_row',
name: 'END',
},
{
type: 'field_custom_label',
name: 'OR0',
align: 'CENTRE',
text: 'or',
},
{
type: 'input_value',
name: 'OR2INP',
check: 'Boolean',
align: 'RIGHT',
},
],
output: 'Boolean',
mutator: 'custom_or_mutator',
style: 'custom_or_style',
inputsInline: true,
},
{
type: 'or_start',
align: 'LEFT',
message0: 'Condition 1 or 2 %1',
args0: [
{
type: 'input_dummy',
name: 'STACK',
},
],
nextStatement: null,
enableContextMenu: false,
style: 'custom_or_style',
tooltip: '',
},
// Block representing the item statement in the STATE mutator.
{
type: 'or_item',
align: 'LEFT',
message0: '+ Condition',
previousStatement: null,
nextStatement: null,
enableContextMenu: false,
style: 'custom_or_style',
tooltip: '',
},
]);

const OR_MUTATOR_MIXIN = {
itemCount_: 0,

saveExtraState: function () {
return {
itemCount: this.itemCount_,
};
},

loadExtraState: function (state) {
this.itemCount_ = state['itemCount'] || 0;
this.updateShape_();
},

decompose: function (workspace) {
const containerBlock = workspace.newBlock('or_start');
containerBlock.initSvg();
let connection = containerBlock.nextConnection;
for (let i = 0; i < this.itemCount_; i++) {
const itemBlock = workspace.newBlock('or_item');
itemBlock.initSvg();
connection.connect(itemBlock.previousConnection);
connection = itemBlock.nextConnection;
}
return containerBlock;
},

compose: function (containerBlock) {
let clauseBlock = containerBlock.nextConnection.targetBlock();
// Count number of inputs.
this.itemCount_ = 0;

const valueConnections = [null];

while (clauseBlock && !clauseBlock.isInsertionMarker()) {
switch (clauseBlock.type) {
case 'or_item':
this.itemCount_++;
valueConnections.push(clauseBlock.valueConnection_);
break;
default:
throw TypeError('Unknown block type: ' + clauseBlock.type);
}
clauseBlock =
clauseBlock.nextConnection && clauseBlock.nextConnection.targetBlock();
}
this.updateShape_();
// Reconnect any child blocks.
this.reconnectChildBlocks_(valueConnections);
},

saveConnections: function (containerBlock) {
let clauseBlock = containerBlock.nextConnection.targetBlock();
let i = 1;
while (clauseBlock) {
switch (clauseBlock.type) {
case 'or_item': {
const inputItem = this.getInput('ORINP' + i);
clauseBlock.valueConnection_ =
inputItem && inputItem.connection.targetConnection;
i++;
break;
}
default:
throw TypeError('Unknown block type: ' + clauseBlock.type);
}
clauseBlock =
clauseBlock.nextConnection && clauseBlock.nextConnection.targetBlock();
}
},

updateShape_: function () {
// Delete everything.
for (let i = 1; this.getInput('ORINP' + i); i++) {
const inpConn = this.getInput('ORINP' + i).connection.targetConnection;
//delete compare Blocks here, when it's a inlineIfCompare with no changes
if (
inpConn &&
inpConn?.sourceBlock_?.type === 'inlineIfCompare' &&
!inpConn?.sourceBlock_.getInput('VAR').connection.targetConnection
) {
inpConn.sourceBlock_.dispose();
}
this.removeInput('OR' + i);
this.removeInput('ORINP' + i);
this.removeInput('END' + i);
}
// Rebuild block.
for (let i = 1; i <= this.itemCount_; i++) {
this.appendEndRowInput('END' + i);
this.appendDummyInput('OR' + i).appendField(new MyCustomLabelField('or'));
this.appendValueInput('ORINP' + i).setAlign(Blockly.inputs.Align.RIGHT);
}
},

reconnectChildBlocks_: function (valueConnections) {
for (let i = 1; i <= this.itemCount_; i++) {
valueConnections[i]?.reconnect(this, 'ORINP' + i);
}
//set compare Blocks here, when the input is empty
//Blockly.Events.setGroup(true);
for (let i = 1; i <= this.itemCount_; i++) {
const inp = this.getInput('ORINP' + i);
if (inp.connection.targetConnection) {
continue;
}
const compBlock = this.workspace.newBlock('inlineIfCompare');
compBlock.outputConnection.connect(inp.connection);
compBlock.initSvg();
compBlock.render();
}
//Blockly.Events.setGroup(false);
},
};

Blockly.Extensions.registerMutator(
'custom_or_mutator',
OR_MUTATOR_MIXIN,
null,
['or_item']
);

And the look of the block is attached to this message, the block marked blue is a 'inlineIfCompare'

Generally it works but it always messes up the undo- und especially the redo-history. Sometimes when an undo tries to insert a block back into the filled inputs it jumps in and out of the input until I spam undo. And the redo chain is able to get cut which results in losing the remaining redos. The inserted compare-blocks also disappear in the inputs while redoing and sometimes they also appear somewhere else on the workspace.

Does someone know how to get the mutator right so that the undo and redo history does work?
I tried some Events.setGroup, Events.disable/enable und recordUndo combinations but with no breakthrough sadly.

I hope someone can help me with this issue.

Best Greetings, Rico
or_block.png

ewpa...@gmail.com

unread,
Jul 15, 2026, 8:38:53 AMJul 15
to Blockly
Hello Rico,

We have a similar feature in App Inventor with mutator blocks that create new blocks (we also have a mutatable OR operator, but it doesn't create blocks). However, I noticed in our latest update that it also seems to be broken in a way you mention. The undo stack looks much messier than I would expect, so it may be an interaction of different changes causing your issue. Have you had a look at the state of your workspace's undoStack_ and whether it makes sense to you? In our case, I see a bunch of extra move operations with different groupIds that are splitting things up.

Cheers,
Evan

Zoë Spriggs

unread,
Jul 15, 2026, 1:39:43 PMJul 15
to Blockly
Hi Rico and Evan,
Thanks for bringing this up, we’ll look into this!

I tested Rico’s code a bit and I am seeing a lot of move operations as well. However, I would expect to see many moves for the undo/redo operations, since the mutators are destroying and re-creating the blocks, which causes a lot of move events to fire.

Either way, it does seem that there’s a bug, especially if you’re seeing regressions in AppInventor. I just filed an issue to keep track of this. If either of you have the chance to add a comment with your thoughts and exactly what you’re seeing, that would be helpful!

Rico, for your code, I’ll just also note that Blockly’s default way of handling this type of situation is (as you suggested) with shadow blocks. With shadow blocks, users can insert their own blocks into inputs or replace the blocks entirely. But if you want to both be able to replace an input with a block *and* be able to replace the entire block, then you’re right that shadow blocks won’t fully suit your needs. If it’s helpful in your case, we also do have this plugin that converts shadow blocks to real blocks when a shadow block is edited. Unfortunately, connecting real blocks as children of shadow blocks isn’t part of this plugin.

Feel free to update the linked issue or this thread if you notice anything else or want to elaborate on what you’re seeing.

Best,
Zoë

Taurus Rico

unread,
Jul 17, 2026, 7:56:25 AMJul 17
to Blockly
Hello Zoë and Evan,

thanks for your replies! 
Good to know, that the problem doesn't come from my code although that likely would be easier to fix.

I did some more testing while outputting the undo and redo stack.

I think the move events are correct because every connect and disconnect from updateShape_ and reconnectChildBlocks_ seems to send a move event.

Differing from my initial post, doing undos seems fine and works like I would expect as long as I just do undo operations without redos in between.
While just undoing I sometimes need to press undo two times to get one change undone but I guess that comes from the mutator block rebuild logic, I'm fine with that.
My described undo-problem from the initial post only occurs when I redo some actions und then follow up with undos. 

All the redo related problems are still relevant, so I think it's mainly a problem with the redo stack.
But I can't really figure out where it's going south with the redo stack.
Sometimes the redo stack just get's cleared completly and the other times I just don't get what I would expect from a redo action.

If I manage to find out more, I'll let you know.

Best Greeting, Rico
Reply all
Reply to author
Forward
0 new messages