Custom Blocks: Style Guide

Over the years the Blockly and Blockly Games team has learned many lessons which are applicable to those developing new blocks. The following are a collection of mistakes we have made, or mistakes commonly made by others.

These are general lessons we've learned using Blockly's visual style and may not apply to all use cases or designs. Other solutions exist. This is also not an exhaustive list of problems users may encounter and how to avoid them. Every case is a little bit different and has its own trade-offs.

1. Conditionals vs. Loops

The most difficult blocks for new users are conditionals and loops. Many block-based environments group both of these blocks into the same 'Controls' category, with both blocks having the same shape and the same colour. This often leads to frustration as new users confuse the two blocks. Blockly recommends moving conditionals and loops into separate 'Logic' and 'Loops' categories, each with a different colour. This makes it clear that these are distinct ideas that behave differently, despite having similar shapes.

Recommendation: Keep conditionals and loops separate.

2. One-based Lists

Novice programmers react badly when they encounter zero-based lists for the first time. As a result, Blockly follows the lead of Lua and Lambda Moo by making list and string indexing one-based.

For more advanced uses of Blockly, zero-based lists are supported to make transitioning to text easier. For younger or more novice audiences one-based indexing is still recommended.

Recommendation: One is the first number.

3. User inputs

There are three ways to obtain a parameter from the user. A dropdown is the most restrictive and is good for simple tutorials and exercises. An input field allows for more freedom and is good for more creative activities. A value block input (usually with a shadow block) offers the opportunity to compute a value (e.g. a random generator) instead of just being a static value.

Recommendation: Choose an input method appropriate to your users.

4. Live block images

Documentation for blocks should include images of the blocks it is referring to. Taking screenshots is easy. But if there are 50 such images, and the application is translated into 50 languages, suddenly one is maintaining 2,500 static images. Then the colour scheme changes, and 2,500 images need updating -- again.

To extract ourselves from this maintenance nightmare, Blockly Games replaced all screenshots with instances of Blockly running in readonly mode. The result looks identical to a picture, but is guaranteed to be up to date. Readonly mode has made internationalization possible.

Recommendation: If you support more than one language, use readonly mode.

5. Your other Left

Feedback from children in the US (though interestingly not from other nations) revealed rampant confusion between left and right. This was resolved with the addition of arrows. If direction is relative (to an avatar, for example) the style of arrow is important. A → straight arrow or a ↱ turn arrow is confusing when the avatar is facing the opposite direction. Most helpful is a ⟳ circular arrow, even in cases where the angle turned is smaller than the arrow indicates.

Recommendation: Supplement text with Unicode icons where possible.

6. High-level Blocks

Wherever possible a higher-level approach should be taken, even if it reduces execution performance or flexibility. Consider this Apps Script expression:

SpreadsheetApp.getActiveSheet().getDataRange().getValues()

Under a 1:1 mapping which preserves all potential capabilities, the above expression would be built using four blocks. But Blockly aims for a higher-level and would provide one block that encapsulates the entire expression. The goal is to optimize for the 95% case, even if it makes the remaining 5% more difficult. Blockly is not intended to be a replacement for text-based languages, it is intended to help users get over the initial learning curve so that they can use text-based languages.

Recommendation: Don't blindly convert your entire API into blocks.

7. Optional Return Values

Many functions in text-based programming perform an action, then return a value. This return value may or may not be used. An example is a stack's pop() function. Pop might be called to get and remove the last element, or it might be called to just remove the last element with the return value being ignored.

var last = stack.pop();  // Get and remove last element.
stack.pop();  // Just remove last element.

Block-based languages are generally not good at ignoring a return value. A value block has to plug into something that accepts the value. There are several strategies to deal with this problem.

a) Steer around the problem. Most block-based languages design the language to avoid these cases. For example, Scratch does not have any blocks that have both side effects and a return value.

b) Provide two blocks. If space in the toolbox is not an issue, a simple solution is to provide two of each of this type of block, one with and one without a return value. The downside is that this can lead to a confusing toolbox with lot of nearly identical blocks.

c) Mutate one block. Use a dropdown, checkbox, or other control allowing the user to choose whether there is a return value or not. The block then changes shape depending on its options. An example of this can be seen in Blockly's list access block.

d) Eat the value. The first version of App Inventor created a special pipe block that ate any connected value. Users didn't understand the concept, and the second version of App Inventor removed the pipe block and instead recommended that users simply assign the value to a throwaway variable.

Recommendation: Each strategy has pros and cons, choose what's right for your users.

8. Growing blocks

Certain blocks may require a variable number of inputs. Examples are an addition block that sums an arbitrary set of numbers, or an if/elseif/else block with an arbitrary set of elseif clauses, or a list constructor with an arbitrary number of initialized elements. There are several strategies, each with its advantages and disadvantages.

a) The simplest approach is to make the user compose the block out of smaller blocks. An example would be adding three numbers, by nesting two two-number addition blocks. Another example would be only providing if/else blocks, and making the user nest them to create elseif conditions.

The advantage of this approach is its initial simplicity (both for the user and the developer). The disadvantage is that in cases where there are a large number of nestings, code becomes very cumbersome and difficult for the user to read and maintain.

b) An alternative is to dynamically expand the block so that there is always one free input at the end. Likewise, the block deletes the last input if there are two free inputs at the end. This is the approach that the first version of App Inventor used.

Blocks that grew automatically were disliked by App Inventor's users for a couple of reasons. First, there was always a free input and the program was never 'complete'. Second, inserting an element in the middle of the stack was frustrating since it involved disconnecting all elements below the edit and reconnecting them. That said, if order is not important, and users can be made comfortable with holes in their program, this is a very convenient option.

c) To solve the hole problem, some developers add +/- buttons to blocks that manually add or remove inputs. Open Roberta uses two such buttons to add or remove inputs from the bottom. Other developers add two buttons at each row so that insertion and deletion from the middle of the stack may be accommodated. Others add two up/down buttons at each row so that reordering of the stack may be accommodated.

This strategy is a spectrum of options ranging from just two buttons per block, through to four buttons per row. At one end is the danger that users aren't able to perform the actions they need, at the other end the UI is so filled with buttons that it looks like the bridge of the starship Enterprise.

d) The most flexible approach is to add a mutator bubble to the block. This is represented as a single button that opens a configuration dialog for that block. Elements may be added, deleted, or rearranged at will.

The disadvantage of this approach is that it mutators are not intuitive for novice users. Introducing mutators requires some form of instruction. Blockly-based applications targeting younger children should not use mutators. Though once learned, they are invaluable for power users.

Recommendation: Each strategy has pros and cons, choose what's right for your users.

9. Clean Code Generation

Advanced Blockly users should be able to look at the generated code (JavaScript, Python, PHP, Lua, Dart, etc) and immediately recognize the program they wrote. This means extra effort needs to be made to keep this machine-generated code readable. Superfluous parentheses, numeric variables, crushed whitespace and verbose code templates all get in the way of producing elegant code. Generated code should include comments and should conform to Google's style guides.

Recommendation: Be proud of your generated code. Show it to the user.

10. Language Dependence

A side effect of the desire for clean code is that Blockly's behaviour is largely defined in terms of how the cross-compiled language behaves. The most common output language is JavaScript, but if Blockly were to cross-compile to a different language, no unreasonable attempts should be made to preserve the exact behaviour across both languages. For example, in JavaScript an empty string is false, whereas in Lua it is true. Defining a single pattern of behaviour for Blockly's code to execute regardless of the target language would result in unmaintainable code that looks like it came out of the GWT compiler.

Recommendation: Blockly is not a language, allow the existing language to affect behaviour.