Creating a new field type

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:

  1. Implement a constructor.
  2. Register a JSON key and implement fromJson.
  3. Handle initialization of the on-block UI and event listeners.
  4. Handle disposal of event listeners (UI disposal is handled for you).
  5. Implement value handling.
  6. Add a text-representation of your field's value, for accessibility.
  7. Add additional functionality such as:

  8. 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:

'use strict';

goog.provide('CustomFields.GenericField');

goog.require('Blockly.Field');

CustomFields.GenericField = function(opt_value, opt_validator) {
  opt_value = this.doClassValidation_(opt_value);
  if (opt_value === null) {
    opt_value = CustomFields.GenericField.DEFAULT_VALUE;
  }  // Else the original value is fine.

  CustomFields.GenericField.superClass_.constructor.call(
      this, opt_value, opt_validator);
};
goog.inherits(CustomFields.GenericField, Blockly.Field);

Method signature

Field constructors usually take in a value and a local validator. If there is a logical default value for your field then the value is typically optional. The validator parameter is only present for editable fields and is typically marked as optional.

Structure

The logic inside of your constructor should follow this flow:

  1. Validate the passed value with this.doClassValidation_ (see class validator).

    The value of a field is always stored as a single object. So, if you have multiple values proved individually in the constructor, you should compile them into a single object before validation.

  2. Handle invalid values by either setting a default value or throwing a clear error.

  3. Optional: Apply additional customization (for example, Label fields allow a css class to be passed, which is then applied to the text).

  4. Call the inherited super constructor (all custom fields should inhertit from Blockly.Field) to properly initialize the value and set the local validator for your field.

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.Fields.register to add your field type to this map, passing in the field class as the second argument:

Blockly.Field.register('field_generic', CustomFields.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.

CustomFields.GenericField.fromJson = function(options) {
  var value = Blockly.utils.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, or Date), 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 mouse down 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.

Blockly.FieldNumberDrag.prototype.bindEvents_ = function() {
  Blockly.FieldNumberDrag.superClass_.bindEvents_.call(this);
  this.mouseDownWrapper_ =
    Blockly.bindEventWithChecks_(this.getClickTarget_(), 'mousedown', this,
        function(event) {
          this.originalMouseX_ = event.clientX;
          this.isMouseDown_ = true;
          this.originalValue_ = this.getValue();
          event.stopPropagation();
        }
    );
  this.mouseMoveWrapper_ =
    Blockly.bindEventWithChecks_(document, 'mousemove', this,
        function(event) {
          if (!this.isMouseDown_) {
            return;
          }
          var delta = event.clientX - this.originalMouseX_;
          this.setValue(this.originalValue_ + delta);
        }
    );
  this.mouseUpWrapper_ =
    Blockly.bindEventWithChecks_(document, 'mouseup', this,
        function(_event) {
          this.isMouseDown_ = false;
        }
    );
};

To bind to an event you should generally use the Blockly.bindEventWithChecks_ 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.bindEvent_ 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

Flowchart describing the order in which validators are run

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 and always return a value.

To implement your field's class validator, override the doClassValidation_ function.

CustomFields.GenericField.prototype.doClassValidation_ = function(newValue) {
  if (typeof newValue != 'string') {
    return null;
  }
  return newValue;
};

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.

CustomFields.FieldTurtle.prototype.doClassValidation_ = function(newValue) {
  if (CustomFields.FieldTurtle.PATTERNS.indexOf(newValue.pattern) == -1) {
    newValue.pattern = null;
  }

  if (CustomFields.FieldTurtle.HATS.indexOf(newValue.hat) == -1) {
    newValue.hat = null;
  }

  if (CustomFields.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.

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 to newValue.
  • Sets the isDirty_ property to true.

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_:

CustomFields.GenericField.prototype.doValueUpdate_ = function(newValue) {
  CustomFields.GenericField.superClass_.
      doValueUpdate_.call(this, 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_.

CustomFields.GenericField.prototype.doValueInvalid_ = function(newValue) {
  this.displayValue_ = newValue;
  this.isDirty_ = true;
  this.isValueValid_ = false;
}

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.

CustomFields.FieldTurtle.prototype.getText = function() {
  var text = this.value_.turtleName + ' wearing a ' + this.value_.hat;
  if (this.value_.hat == 'Stovepipe' || this.value_.hat == 'Propeller') {
    text += ' hat';
  }
  return text;
};

If the text of your field is different than the value of your field, you should override the getTextfunction to provide the correct 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.

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.

Image of angle picker

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

Image of text input editor

CustomFields.GenericField.prototype.showEditor_ = function() {
  // 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

Blockly.GenericField.prototype.showEditor_ = function() {
  // 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.DIV.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.

CustomFields.FieldTurtle.prototype.widgetDispose_ = function() {
  for (var i = this.editorListeners_.length, listener;
      listener = this.editorListeners_[i]; i--) {
    Blockly.unbindEvent_(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.

Flowchart describing how to make decision of whether to override render_

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:

  1. DOM creation should be handled during initialization, as it is more efficient.
  2. You should always update the size_ property to match the on-block display's size.
      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.

CustomFields.FieldTurtle.prototype.updateSize_ = function() {
  var size = this.movableGroup_.getBBox();
  if (this.borderRect_) {
    this.borderRect_.setAttribute('width',
      size.width + Blockly.BlockSvg.SEP_SPACE_X);
    this.borderRect_.setAttribute('height', size.height + 9);
  }

  this.size_.width = size.width;
  this.size_.height = size.height + 10 +
      (Blockly.BlockSvg.INLINE_PADDING_Y * 2);
};

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 updateColour method. You will want to use the getColour suite of functions.

Blockly.FieldDropdown.prototype.updateColour = function() {
  if (this.sourceBlock_.isShadow()) {
    this.arrow_.style.fill = this.sourceBlock_.getColourShadow();
  } else {
    this.arrow_.style.fill = this.sourceBlock_.getColour();
  }
};

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.

CustomFields.FieldTurtle.prototype.updateEditable = function() {
  if (!this.fieldGroup_) {
    // Not initialized yet.
    return;
  }
  Blockly.FieldColour.superClass_.updateEditable.call(this);

  var group = this.getClickTarget_();
  if (!this.isCurrentlyEditable()) {
    group.style.cursor = 'not-allowed';
  } else {
    group.style.cursor = this.CURSOR;
  }
};

Serialization and XML

If your field has a primitive as its value, and your field's class validator is willing to accept that value as a string (e.g. 'true' instead of true) you do not need to do custom serialization.

Otherwise you will need to override the toXml and fromXml functions and set the SERIALIZABLEproperty to true. Generally whatever is closest to the field's "value" is put inside the textContent of the provided XML element, but it could be applied to an attribute as well.

CustomFields.FieldTurtle.prototype.toXml = function(fieldElement) {
  fieldElement.setAttribute('pattern', this.value_.pattern);
  fieldElement.setAttribute('hat', this.value_.hat);
  fieldElement.textContent = this.value_.turtleName;

  return fieldElement;
};

CustomFields.FieldTurtle.prototype.fromXml = function(fieldElement) {
  var value = {};

  value.pattern = fieldElement.getAttribute('pattern');
  value.hat = fieldElement.getAttribute('hat');
  value.turtleName = fieldElement.textContent;

  this.setValue(value);
};

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 to XML. It defaults to false. If this property is true, you may need to provide serialization and deserialization functions (see Serialization and XML).

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.