Before creating a new field type, consider if one of the other methods for customizing fields suits your needs. If your application needs to store a new value type, or you wish to create a new UI for an existing value type, you probably need to create a new field type.
To create a new field, do the following:
- Implement a constructor.
- Register a JSON key and implement
fromJson
. - Handle initialization of the on-block UI and event listeners.
- Handle disposal of event listeners (UI disposal is handled for you).
- Implement value handling.
- Add a text-representation of your field's value, for accessibility.
- Add additional functionality such as:
- Configure additional aspects of your field, such as:
This section assumes that you have read and are familiar with the contents in Anatomy of a Field.
For an example of a custom field see the Custom Fields demo .
Implementing a constructor
The field's constructor is responsible for setting up the field's initial value and optionally setting up a local validator. The custom field's constructor is called during the source block initialization regardless of whether the source block is defined in JSON or JavaScript. So, the custom field doesn't have access to the source block during construction.
The following code sample creates a custom field named GenericField
:
class GenericField extends Blockly.Field {
constructor(value, validator) {
super(value, validator);
this.SERIALIZABLE = true;
}
}
Method signature
Field constructors usually take in a value and a local validator. The value is
optional, and if you don't pass a value (or you pass a value that fails class
validation) then the default value of the superclass will be used. For the
default Field
class, that value is null
. If you don't want that default
value, then be sure to pass a suitable value. The validator parameter is only
present for editable fields and is typically marked as optional. Learn more
about validators in the Validators
docs.
Structure
The logic inside of your constructor should follow this flow:
- Call the inherited super constructor (all custom fields should inherit from
Blockly.Field
or one of its subclasses) to properly initialize the value and set the local validator for your field. - If your field is serializable, set the corresponding property in the constructor. Editable fields must be serializable, and fields are editable by default, so you should probably set this property to true unless you know it shouldn't be serializable.
- Optional: Apply additional customization (for example, Label fields allow a css class to be passed, which is then applied to the text).
JSON and registration
In JSON block
definitions,
fields are described by a string (e.g. field_number
, field_textinput
).
Blockly maintains a map from these strings to field objects, and calls
fromJson
on the appropriate object during construction.
Call Blockly.fieldRegistry.register
to add your field type to this map,
passing in the field class as the second argument:
Blockly.fieldRegistry.register('field_generic', GenericField);
You also need to define your fromJson
function. Your implementation should
first dereference any string
table
references using
replaceMessageReferences,
and then pass the values to the constructor.
GenericField.fromJson = function(options) {
const value = Blockly.utils.parsing.replaceMessageReferences(
options['value']);
return new CustomFields.GenericField(value);
};
Initializing
When your field is constructed, it basically only contains a value. Initialization is where the DOM is built, the model is built (if the field possesses a model), and events are bound.
On-Block display
During initialization you are responsible for creating anything you will need for the field's on-block display.
Defaults, background, and text
The default initView
function creates a light coloured rect
element and a
text
element. If you want your field to have both of these, plus some extra
goodies, call the superclass initView
function before adding the rest of your
DOM elements. If you want your field to have one, but not both, of these
elements you can use the createBorderRect_
or createTextElement_
functions.
Customizing DOM construction
If your field is a generic text field (e.g. Text
Input),
DOM construction will be handled for you. Otherwise you will need to override
the initView
function to create the DOM elements that you will need during
future rendering of your field.
For example, a dropdown field may contain both images and text. In initView
it
creates a single image element and a single text element. Then during render_
it shows the active element and hides the other, based on the type of the
selected option.
Creating DOM elements can either be done using the
Blockly.utils.dom.createSvgElement
method, or using traditional DOM creation
methods.
The requirements of a field's on-block display are:
- All DOM elements must be children of the field's
fieldGroup_
. The field group is created automatically. - All DOM elements must stay inside the reported dimensions of the field.
See the Rendering section for more details on customizing and updating your on-block display.
Adding Text Symbols
If you want to add symbols to a field's text (such as the
Angle
field's degree symbol) you can append the symbol element (usually contained in a
<tspan>
) directly to the field's textElement_
.
Input events
By default fields register tooltip events, and mousedown events (to be used for
showing
editors).
If you want to listen for other kinds of events (e.g. if you want to handle
dragging on a field) you should override the field's bindEvents_
function.
bindEvents_() {
// Call the superclass function to preserve the default behavior as well.
super.bindEvents_();
// Then register your own additional event listeners.
this.mouseDownWrapper_ =
Blockly.browserEvents.conditionalBind(this.getClickTarget_(), 'mousedown', this,
function(event) {
this.originalMouseX_ = event.clientX;
this.isMouseDown_ = true;
this.originalValue_ = this.getValue();
event.stopPropagation();
}
);
this.mouseMoveWrapper_ =
Blockly.browserEvents.conditionalBind(document, 'mousemove', this,
function(event) {
if (!this.isMouseDown_) {
return;
}
var delta = event.clientX - this.originalMouseX_;
this.setValue(this.originalValue_ + delta);
}
);
this.mouseUpWrapper_ =
Blockly.browserEvents.conditionalBind(document, 'mouseup', this,
function(_event) {
this.isMouseDown_ = false;
}
);
}
To bind to an event you should generally use the
Blockly.utils.browserEvents.conditionalBind
function. This method of binding events filters out secondary touches during
drags. If you want your handler to run even in the middle of an in-progress drag
you can use the
Blockly.browserEvents.bind
function.
Disposing
If you registered any custom event listeners inside the field's bindEvents_
function they will need to be unregistered inside the dispose
function.
If you correctly initialized the
view
of your field (by appending all DOM elements to the fieldGroup_
), then the
field's DOM will be disposed of automatically.
Value Handling
→ For information about a field's value vs its text see Anatomy of a field.
Validation order
Implementing a class validator
Fields should only accept certain values. For example, number fields should only accept numbers, colour fields should only accept colours etc. This is ensured through class and local validators. The class validator follows the same rules as local validators except that it is also run in the constructor and, as such, it should not reference the source block.
To implement your field's class validator, override the doClassValidation_
function.
doClassValidation_(newValue) {
if (typeof newValue != 'string') {
return null;
}
return newValue;
};
Handling valid values
If the value passed to a field with setValue
is valid you will receive a
doValueUpdate_
callback. By default the doValueUpdate_
function:
- Sets the
value_
property tonewValue
. - Sets the
isDirty_
property totrue
.
If you simply need to store the value, and don't want to do any custom handling,
you do not need to override doValueUpdate_
.
Otherwise, if you wish to do things like:
- Custom storage of
newValue
. - Change other properties based on
newValue
. - Save whether the current value is valid or not.
You will need to override doValueUpdate_
:
doValueUpdate_(newValue) {
super.doValueUpdate_(newValue);
this.displayValue_ = newValue;
this.isValueValid_ = true;
}
Handling invalid values
If the value passed to the field with setValue
is invalid you will receive a
doValueInvalid_
callback. By default the doValueInvalid_
function does
nothing. This means that by default invalid values will not be shown. It also
means the field will not be rerendered, because the
isDirty_
property will not be set.
If you wish to display invalid values you should override doValueInvalid_
.
Under most circumstances you should set a displayValue_
property to the
invalid value, set
isDirty_
to true
, and override
render_
for the on-block display to update based on the displayValue_
instead of the
value_
.
doValueInvalid_(newValue) {
this.displayValue_ = newValue;
this.isDirty_ = true;
this.isValueValid_ = false;
}
Multi-part values
When your field contains a multipart value (e.g. lists, vectors, objects) you may wish the parts to be handled like individual values.
doClassValidation_(newValue) {
if (FieldTurtle.PATTERNS.indexOf(newValue.pattern) == -1) {
newValue.pattern = null;
}
if (FieldTurtle.HATS.indexOf(newValue.hat) == -1) {
newValue.hat = null;
}
if (FieldTurtle.NAMES.indexOf(newValue.turtleName) == -1) {
newValue.turtleName = null;
}
if (!newValue.pattern || !newValue.hat || !newValue.turtleName) {
this.cachedValidatedValue_ = newValue;
return null;
}
return newValue;
}
In the above example each property of newValue
is validated individually. Then
at the end of the doClassValidation_
function, if any individual property is
invalid, the value is cached to the cacheValidatedValue_
property before
returning null
(invalid). Caching the object with individually validated
properties allows the
doValueInvalid_
function to handle them separately, simply by doing a
!this.cacheValidatedValue_.property
check, instead of re-validating each
property individually.
This pattern for validating multi-part values can also be used in local validators but currently there is no way to enforce this pattern.
isDirty_
isDirty_
is a flag used in the
setValue
function, as well as other parts of the field, to tell if the field needs to be
re-rendered. If the field's display value has changed, isDirty_
should usually
be set to true
.
Text
→ For information about where a field's text is used and how it is different from the field's value, see Anatomy of a field.
If the text of your field is different than the value of your field, you should
override the
getText
function
to provide the correct text.
getText() {
let text = this.value_.turtleName + ' wearing a ' + this.value_.hat;
if (this.value_.hat == 'Stovepipe' || this.value_.hat == 'Propeller') {
text += ' hat';
}
return text;
}
Creating an editor
If you define the showEditor_
function, Blockly will automatically listen for
clicks and call showEditor_
at the appropriate time. You can display any HTML
in your editor by wrapping it one of two special divs, called the DropDownDiv
and WidgetDiv, which float above the rest of Blockly's UI.
DropDownDiv vs WidgetDiv
The DropDownDiv
is used to provide editors that live inside of a box connected
to a field. It automatically positions itself to be near the field while staying
within visible bounds. The angle picker and color picker are good examples of
the DropDownDiv
.
The WidgetDiv
is used to
provide editors that do not live inside of a box. Number fields use the
WidgetDiv to cover the field with an HTML text input box. While the DropDownDiv
handles positioning for you, the WidgetDiv does not. Elements will need to be
manually positioned. The coordinate system is in pixel coordinates relative to
the top left of the window. The text input editor is a good example of the
WidgetDiv
.
DropDownDiv sample code
showEditor_() {
// Create the widget HTML
this.editor_ = this.dropdownCreate_();
Blockly.DropDownDiv.getContentDiv().appendChild(this.editor_);
// Set the dropdown's background colour.
// This can be used to make it match the colour of the field.
Blockly.DropDownDiv.setColour('white', 'silver');
// Show it next to the field. Always pass a dispose function.
Blockly.DropDownDiv.showPositionedByField(
this, this.disposeWidget_.bind(this));
}
WidgetDiv sample code
showEditor_() {
// Show the div. This automatically closes the dropdown if it is open.
// Always pass a dispose function.
Blockly.WidgetDiv.show(
this, this.sourceBlock_.RTL, this.widgetDispose_.bind(this));
// Create the widget HTML.
var widget = this.createWidget_();
Blockly.WidgetDiv.getDiv().appendChild(widget);
}
Cleaning up
Both the DropDownDiv and the WidgetDiv handle destroying the widget HTML elements, but you need to manually dispose of any event listeners you have applied to those elements.
widgetDispose_() {
for (let i = this.editorListeners_.length, listener;
listener = this.editorListeners_[i]; i--) {
Blockly.browserEvents.unbind(listener);
this.editorListeners_.pop();
}
}
The dispose
function is called in a null
context on the DropDownDiv
. On
the WidgetDiv
it is called in the context of the WidgetDiv
. In either case
it is best to use the
bind
function when passing a dispose function, as shown in the above DropDownDiv
and WidgetDiv
examples.
→ For information about disposing not specific to disposing of editors see Disposing.
Updating the on-block display
The render_
function is used to update the field's on-block display to match
its internal value.
Common examples include:
- Change the text (dropdown)
- Change the color (color)
Defaults
The default render_
function sets the display text to the result of the
getDisplayText_
function. The getDisplayText_
function returns the field's value_
property
cast to a string, after it has been truncated to respect the maximum text
length.
If you are using the default on-block display, and the default text behavior
works for your field, you do not need to override render_
.
If the default text behavior works for your field, but your field's on-block
display has additional static elements, you can call the default render_
function, but you will still need to override it to update the field's
size.
If the default text behavior does not work for your field, or your field's
on-block display has additional dynamic elements, you will need to customize
the render_
function.
Customizing rendering
If the default rendering behavior does not work for your field, you will need to define custom rendering behavior. This can involve anything from setting custom display text, to changing image elements, to updating background colours.
All DOM attribute changes are legal, the only two things to remember are:
- DOM creation should be handled during initialization, as it is more efficient.
- You should always update the
size_
property to match the on-block display's size.
render_() {
switch(this.value_.hat) {
case 'Stovepipe':
this.stovepipe_.style.display = '';
break;
case 'Crown':
this.crown_.style.display = '';
break;
case 'Mask':
this.mask_.style.display = '';
break;
case 'Propeller':
this.propeller_.style.display = '';
break;
case 'Fedora':
this.fedora_.style.display = '';
break;
}
switch(this.value_.pattern) {
case 'Dots':
this.shellPattern_.setAttribute('fill', 'url(#polkadots)');
break;
case 'Stripes':
this.shellPattern_.setAttribute('fill', 'url(#stripes)');
break;
case 'Hexagons':
this.shellPattern_.setAttribute('fill', 'url(#hexagons)');
break;
}
this.textContent_.nodeValue = this.value_.turtleName;
this.updateSize_();
}
Updating size
Updating the size_
property of a field is very important, as it informs the
block rendering code how to position the field. The best way to figure out
exactly what that size_
should be is by experimenting.
updateSize_() {
const bbox = this.movableGroup_.getBBox();
let width = bbox.width;
let height = bbox.height;
if (this.borderRect_) {
width += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2;
height += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2;
this.borderRect_.setAttribute('width', width);
this.borderRect_.setAttribute('height', height);
}
// Note how both the width and the height can be dynamic.
this.size_.width = width;
this.size_.height = height;
}
Matching block colours
If you want elements of your field to match the colours of the block they are
attached to, you should override the applyColour
method. You will want to
access the colour through the block's style property.
applyColour() {
const sourceBlock = this.sourceBlock_;
if (sourceBlock.isShadow()) {
this.arrow_.style.fill = sourceBlock.style.colourSecondary;
} else {
this.arrow_.style.fill = sourceBlock.style.colourPrimary;
}
}
Updating editability
The updateEditable
function can be used to change how your field appears
depending on if it is editable or not. The default function makes it so the
background does/doesn't have a hover response (border) if it is/isn't editable.
The on-block display should not change size depending on its editability, but
all other changes are allowed.
updateEditable() {
if (!this.fieldGroup_) {
// Not initialized yet.
return;
}
super.updateEditable();
const group = this.getClickTarget_();
if (!this.isCurrentlyEditable()) {
group.style.cursor = 'not-allowed';
} else {
group.style.cursor = this.CURSOR;
}
}
Serialization
Serialization is about saving the state of your field so that it can be reloaded into the workspace later.
The state of your workspace always includes the field's value, but it could also include other state, such as the state of your field's UI. For example, if your field was a zoomable map that allowed the user to select countries, you could also serialize the zoom level.
If your field is serializable, you must set the SERIALIZABLE
property to
true
.
Blockly provides two sets of serialization hooks for fields. One pair of hooks works with the new JSON serialization system, and the other pair works with the old XML serialization system.
saveState
and loadState
saveState
and loadState
are serialization hooks that work with the new JSON
serialization system.
In some cases you do not need to provide these, because the default
implementations will work. If (1) your field is a direct subclass of the base
Blockly.Field
class, (2) your value is a JSON serializable
type, and (3) you only need to
serialize the value, then the default implementation will work just fine!
Otherwise, your saveState
function should return a JSON serializable
object/value which represents the state of the field. And your loadState
function should accept the same JSON serializable object/value, and apply it to
the field.
saveState() {
return {
'country': this.getValue(), // Value state
'zoom': this.getZoomLevel(), // UI state
};
}
loadState(state) {
this.setValue(state['country']);
this.setZoomLevel(state['zoom']);
}
Full serialization and backing data
saveState
also receives an optional parameter doFullSerialization
. This is
used by fields that normally reference state serialized by a different
serializer (like backing data models). The parameter signals that
the referenced state won't be available when the block is deserialized, so the
field should do all of the serialization itself. For example, this is true when
an individual block is serialized, or when a block is copy-pasted.
Two common use cases for this are:
- When an individual block is loaded into a workspace where the backing data model doesn't exist, the field has enough information in its own state to create a new data model.
- When a block is copy-pasted, the field always creates a new backing data model instead of referencing an existing one.
One field that uses this is the built-in variable field. Normally it serializes
the ID of the variable it is referencing, but if doFullSerialization
is true
it serializes all of its state.
saveState(doFullSerialization) {
const state = {'id': this.variable_.getId()};
if (doFullSerialization) {
state['name'] = this.variable_.name;
state['type'] = this.variable_.type;
}
return state;
}
loadState(state) {
const variable = Blockly.Variables.getOrCreateVariablePackage(
this.getSourceBlock().workspace,
state['id'],
state['name'], // May not exist.
state['type']); // May not exist.
this.setValue(variable.getId());
}
The variable field does this to make sure that if it is loaded into a workspace where its variable doesn't exist, it can create a new variable to reference.
toXml
and fromXml
toXml
and fromXml
are serialization hooks that work with the old XML
serialization system. Only use these hooks if you have to (e.g. you're working
on an old codebase that hasn't migrated yet), otherwise use saveState
and
loadState
.
Your toXml
function should return an XML node which represents the state of
the field. And your fromXml
function should accept the same XML node and apply
it to the field.
toXml(fieldElement) {
fieldElement.textContent = this.getValue();
fieldElement.setAttribute('zoom', this.getZoomLevel());
return fieldElement;
}
fromXml(fieldElement) {
this.setValue(fieldElement.textContent);
this.setZoomLevel(fieldElement.getAttribute('zoom'));
}
Editable and serializable properties
The EDITABLE
property determines if the field should have UI to indicate that
it can be interacted with. It defaults to true
.
The SERIALIZABLE
property determines if the field should be serialized. It
defaults to false
. If this property is true
, you may need to provide
serialization and deserialization functions (see
Serialization).
Customizing the cursor
The CURSOR
property determines the cursor the users see when they hover over
your field. It should be a valid CSS cursor string. This defaults to the cursor
defined by .blocklyDraggable
, which is the grab cursor.