Connection checks restrict which connections (and therefor blocks) can connect to each other.
Connection checks are useful for modeling types. For example, the following three blocks have no business being connected, because they represent code that returns different types:
Connection checks can be used to prevent these blocks from connecting. This gives users instantaneous feedback and prevents many simple mistakes.
How they work
Every connection can be associated with a "connection check" which is a nullable array of strings.
Two connections can connect if:
- They are compatible types (e.g. an output connecting to an input).
- They have at least one string in their connection check in common.
For example, the following two checks could connect, because they share the
'apple'
string:
['apple', 'ball', 'cat']
['apple', 'bear', 'caterpillar']
But these two checks couldn't connect, because they don't share any strings:
['apple', 'ball', 'cat']
['ape', 'bear', 'caterpillar']
There is one other special case. If either array is null
, then the two
connections can also connect. This lets you define connections that can connect
to anything.
null
['ape', 'bear', 'caterpillar]
Set checks
By default, all connections have a null
connection-check, meaning they can
connect to anything. Connection checks need to be assigned manually.
How you assign connection checks to connections is different depending on whether you're using JSON block definitions, or JavaScript block definitions.
JSON
For top-level connections, you assign the check directly to the property that
defines the connection. The value you assign can be null
, a string (which
becomes the only entry in the connection check), or an array of strings.
{
'type': 'custom_block',
'output': null,
'nextStatement': 'a connection check entry',
'previousStatement': ['four', 'connection', 'check', 'entries']
}
For inputs, you can assign the check to a check
property of the input
definition. If the check
property doesn't exist, the check is considered
null
. The value you assign can be a string, or an array of strings.
{
'type': 'custom_block',
'message0': '%1 %2',
'args0': [
{
'type': 'input_value',
'check': 'a connection check entry'
},
{
'type': 'input_statement',
'check': ['four', 'connection', 'check', 'entries']
}
]
}
JavaScript
For top-level connections, you can pass the check directly to the method that
defines the connection. If you don't pass a value, the check is considered
null
. The value you pass can be a string (which becomes the only entry in the
connection check), or an array of strings.
Blockly.Blocks['custom_block'] = {
init: function() {
this.setOutput(true); // null check
this.setNextStatement(true, 'a connection check entry');
this.setPreviousStatement(true, ['four', 'connection', 'check', 'entries']);
}
}
For inputs, you can pass the check to the setCheck
method, after you have
defined the input. If the setCheck
method isn't called, the check is
considered null
. The value you pass can be a string, or an array of strings.
Blockly.Blocks['custom_block'] = {
init: function() {
this.appendValueInput('NAME')
.setCheck('a connection check entry');
this.appendStatementInput('NAME')
.setCheck(['four', 'connection', 'check', 'entries']);
}
}
Built-in check strings
The built-in blocks have connection checks with the values 'Array'
,
'Boolean'
, 'Colour'
, 'Number'
, and 'String'
. If you want your blocks to
interoperate with the built-in blocks, you can use these values to make them
compatible.
Value examples
When you are defining connection checks for inputs and outputs, usually you should think of the checks as representing types.
Inputs' checks should include every "type" they accept, and outputs' checks should include exactly what they "return".
Accept a single type
In the most basic case where you want create a block that "accepts" or "returns" one type, you need to include that type in the connection's connection check.
Accept multiple types
To create a block that "accepts" multiple types, you need to include every accepted type in the input's connection check.
By convention, if an output can sometimes be accepted in multiple situations (e.g. if you allow numbers to sometimes be used as strings) the output should be more restrictive, and the input(s) should be more permissive. This convention makes sure that outputs don't connect where they're not supported.
Accept any type
To create a block that "accepts" any type, you need to set the input's
connection check to null
.
Return subtypes
To create a block that "returns" a subtype, you need to include both the type and the supertype in the output's connection check.
In the case of subtypes, it is okay to have multiple checks in an output check, because the block always "returns" both types.
Return parameterized types
To create a block that "returns" a parameterized type, you need to include both the parameterized version and the unparameterized version in the output's connection check.
Depending on how strict you want your block language to be, you may also want to include the type's variance(s).
Just like with subtypes, it is okay to have multiple checks in an output check in this case, because the block always "returns" both types.
Stack or statement examples
There are a few common ways developers define checks for previous and next connections. Usually you think of these as restricting the ordering of blocks.
Next connections should include which blocks should follow the current one, and previous connections include what the current block "is".
Keep blocks in order
To create a set of blocks that connect in a defined order, you need to include which blocks should follow the current one in the next connection check, and what the current block "is" in the previous connection check.
Allow lots of middle blocks
To create a set of ordered blocks that allow lots of middle blocks, you need to include at least one entry from the middle block's previous connection check in the middle block's next connection check. This allows the block to be followed by more of itself.
Allow no middle blocks
To create a set of ordered blocks where the middle blocks are optional, you need to include at least one entry from both the middle block's previous connection check, and the last block's previous connection check in the first block's next connection check. This allows the first block to be followed by either a middle block, or a last block.
Either-or stacks
To create a block that can only be followed by blocks from one group, or blocks from another (and not both), you need to do two things:
You need to include at least one entry from both of the groups previous connection checks in the first block's next connection check.
You need to define the groups' next connection checks to only include values which are in their previous connection checks (so they can only be followed by blocks of the same group).
Limitations
This system is quite robust and can solve many use-cases, but it does have a few limitations.
Restrict the greater context
This system does not, by itself, support restricting the "greater context" in
which a connection is allowed to connect. For example, you cannot say that a
break
block is only allowed to exist inside of a loop
block. The connection
checking system only considers the immediate two connections being connected.
You can support this by using the event system to listen to block move events and check if the block is incorrectly positioned.
Blockly.Blocks['custom_block'] = {
init: function() { }
onchange: function(e) {
if (this.workspace.isDragging()) return;
if (e.type !== Blockly.Events.BlockMove) return;
if (!this.getSurroundLoop()) this.outputConnection.disconnect();
}
loopTypes: new Set(); // Your valid *block types* (not connection checks).
getSurroundLoop: function () {
let block = this.getSurroundParent();
do {
if (loopTypes.has(block.type)) return block;
block = block.getSurroundParent();
} while (block);
return null;
},
}
Generic types
This system does not, by itself, support defining generic types. For example, you cannot create an "Identity" block, that "returns" whatever its input is.
You can somewhat support this by actively changing the connection check on the block's output to match its input. Which you can do using the event system to listen to block move events.
Blockly.Blocks['custom_block'] = {
init: function() { }
onchange: function(e) {
if (e.type !== Blockly.Events.BlockMove) return;
this.setOutput(
true, this.getInputTargetBlock()?.outputConnection.getCheck());
}
}
But if the connected block is also generic, then this doesn't work correctly. There is no good work around for this case.
Connection checkers
If this system doesn't work for your use case, you can also change how the connection checks are compared by creating a custom connection checker.
For example, if you wanted to create a more advanced system that handles some of the limitations of this one, you can create a custom connection checker.