Programatically add validators to shadow blocks

113 views
Skip to first unread message

francisc...@ubbu.io

unread,
Oct 9, 2024, 11:29:17 AM10/9/24
to Blockly
Hi everyone,

I'm trying to programmatically add a custom validator to a FieldNumber inside a shadow block. This shadow block is connected to another block, and both the shadow block and the connection are created programmatically.

Here’s the code to create and connect the shadow block:

const shadowBlock = workspace.newBlock(type); shadowBlock.setShadow(true); shadowBlock.setFieldValue(value, fieldName); shadowBlock.initSvg(); connection.connect(shadowBlock.outputConnection);

I expected that by doing something like the following, the custom validator would be applied whenever a block was dragged from the flyout:

const shadowField = shadowBlock.getField(fieldName); shadowField?.setValidator(validator);

However, I realized that the connection creates a new shadow block from a shadowDom copy, which doesn’t retain this validator. As a workaround, I implemented the following:

const prevCreateShadowBlock = connection.createShadowBlock.bind(connection); connection.createShadowBlock = (attemptToConnect: boolean) => { const shadowBlock = prevCreateShadowBlock(attemptToConnect) as Blockly.BlockSvg | null; if (shadowBlock) { const shadowField = shadowBlock.getField(fieldName); shadowField?.setValidator(validator); } return shadowBlock; };

This way, every time a shadow block is created, the validator is reapplied to the FieldNumber field. But this feels like a workaround to the shadowDom. Is there a nicer way to do this?

Attached is an example image of the block I'm creating.

Thanks in advance

Screenshot 2024-10-09 at 16.28.04.png

Christopher Allen

unread,
Oct 11, 2024, 11:43:02 AM10/11/24
to blo...@googlegroups.com
Hi Francisco,

I'm trying to programmatically add a custom validator to a FieldNumber inside a shadow block. This shadow block is connected to another block, and both the shadow block and the connection are created programmatically.

Here’s the code to create and connect the shadow block:

const shadowBlock = workspace.newBlock(type); shadowBlock.setShadow(true); shadowBlock.setFieldValue(value, fieldName); shadowBlock.initSvg(); connection.connect(shadowBlock.outputConnection);

I expected that by doing something like the following, the custom validator would be applied whenever a block was dragged from the flyout:

const shadowField = shadowBlock.getField(fieldName); shadowField?.setValidator(validator);

However, I realized that the connection creates a new shadow block from a shadowDom copy, which doesn’t retain this validator.

[Aside: I'm not sure what you mean by "shadowDom" here.  Note that shadow blocks in Blockly have nothing to do with the shadow dom of web components, a web platform feature that Blockly does not use.]

Your code will indeed create a new block, convert it to a shadow, and attach it to connection, and add a field validator.  There are situations where one might want to do that, but this is not the usual way to create blocks and—as you have discovered—the validator is only attached to that specific block, not to others of its type.

Blocks (whether shadow or not) are normally created entirely by Workspace.prototype.newBlock method, which is a wrapper around the Block or BlockSvg constructor which (in summary):
  • Begins with a fresh Block or BlockSvg instance.
  • Uses Object.assign() to mix in properties from the block definition Blockly.Blocks[type].
  • Then calls  this.init(/*...*/) to finish creating the block.
  • The .init() method then makes additional method calls to define the shape of the block by adding inputs, fields, and other connections, setting the colour and so on.
Very broadly, we would expect that in most cases developers will:
  • Define block types by creating entries in Blockly.Blocks[], either directly (for JavaScript block definitions) or via defineBlocksWithJsonArray() (for JSON block definitions).
  • Only create blocks using an existing block definition, without subsequent modification by direct method calls.
    • By implication: normally only ever call setFieldValidator() from the .init() method or from the .compose() method of the block definition (the latter only in the case the block is mutable and a new field has been added by the mutation).
  • Never call setShadow() directly—this will normally only be called by:
    • the toolbox, when creating the blocks specified in the toolbox definition and the entry includes shadow blocks, or
    • when deserializing blocks into the workspace, where one of those blocks was originally a shadow when it was serialized.
If you want to create an initial workspace configuration including shadow blocks, we usually recommend that you do that by calling Blockly.serialization.workspaces.load() and passing a (possibly manually-tweaked) workspace serialisation that includes the desired initial configuration.

As a workaround, I implemented the following:

[...]

This way, every time a shadow block is created, the validator is reapplied to the FieldNumber field. But this feels like a workaround to the shadowDom. Is there a nicer way to do this?

It looks like in this case you should simply include the shadow number blocks in the toolbox entry for the RGB block—see the sections of our documentation about preset blocks and shadow blocks.


Best wishes,

Christopher

Message has been deleted

francisc...@ubbu.io

unread,
Oct 13, 2024, 2:23:19 PM10/13/24
to Blockly
Hi Christopher,

When I said shadowDom I meant the shadowDom property from the Connection class:
/**
   * Returns the xml representation of the connection's shadow block.
   *
   * @param returnCurrent If true, and the shadow block is currently attached to
   *     this connection, this serializes the state of that block and returns it
   *     (so that field values are correct). Otherwise the saved shadowDom is
   *     just returned.
   * @returns Shadow DOM representation of a block or null.
   */
  getShadowDom(returnCurrent?: boolean): Element | null {
    return returnCurrent && this.targetBlock()!.isShadow()
      ? (Xml.blockToDom(this.targetBlock() as Block) as Element)
      : this.shadowDom;
  }

An important detail I overlooked in my assessment is that the shadow blocks and validators are added to the main block through a block extension. I suspect the issue stems from the extensions not being applied during the createShadowBlock function. This is because the shadowDom is only a shallow copy of the block and does not undergo the full creation cycle you mentioned earlier. While my toolbox block correctly applies all mutations and extensions, the shadow copy of the connection does not seem to follow suit. Let me know if it's a bug, or the intended behaviour or if you need some more details/context as to why I'm doing this.

Thank you again for your very helpful feedback!
Reply all
Reply to author
Forward
0 new messages