HomeDev GuideRecipesAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Create an editor widget

Describes how to create a custom Dojo widget for the Optimizely edit page.

Custom editor widgets extend the editing UI with property editors tailored to your content model. Build widgets using either the Dojo and Dijit framework or ES6 modules.

Build a custom editor widget when the built-in property editors do not match the editing experience your content model requires. Before you start, install the Optimizely CMS NuGet packages and configure a client resources path in your project."

Prerequisites

  • CMS 13 project with client resources configured
  • Node.js (for the eslint plugin)
  • Familiarity with Dojo or ES6 modules

Create a simple editor widget

A basic Dojo widget requires a template, value handling, and change notification. The following example creates a widget that validates an email address.

📘

Note

Use the Optimizely eslint-plugin-cms tool to verify that only public non-deprecated Optimizely Content Management System (CMS) APIs are used when creating widgets.

define(
  [
    "dojo/_base/declare",
    "dijit/_Widget",
    "dijit/_TemplatedMixin"
  ], 
  function(
    declare,
    _Widget,
    _TemplatedMixin
  ) {
      return declare([_Widget, _TemplatedMixin], 
        {
          // templateString: [protected] String
          // A string that represents the default widget template.
          templateString: '<div> \
          <input type="email" 
                 data-dojo-attach-point="email" 
                 data-dojo-attach-event="onchange:_onChange" /> \
                 </div>'
        })
    }
);
📘

Note

The code snippet inherits from dijit/_TemplatedMixin, which lets you define the widget template as an inline string or an external HTML file. Attach to template elements for a programmatic node reference. Attach events to event handlers in the widget.

The initial value is unavailable when the widget is created. The framework sets the value with set('value', value) and calls this method multiple times when the editor loads. Declare a _setValueAttr method to update the text field value when the framework sets a value on the widget:

_setValueAttr: function (value) 
  {
    // summary : Sets the value of the widget to "value" and updates the value displayed in the text field.
    // tags    : private
    
    this._set('value', value);
    this.email.value = this.value || '';
  }

_setValueAttr references a variable named email, the text field DOM node. dijit/_TemplatedMixin assigns this variable when it parses data-dojo-attach-point in the template.

Call onChange and pass a value to propagate widget changes. Call onChange during editing as often as needed to keep the preview accurate.

_onChange: function (event) 
  {
    // summary : Handles the text field change event and populates this to the onChange method.
    // tags    : private
    
    this._set('value', event.target.value);
    this.onChange(this.value);
  }

_onChange is a private event handler that fires when the text field value changes. The template wires this handler with the data-dojo-attach-event syntax. After updating the value, the handler calls onChange, which triggers a page update.

onChange: function (value) 
  {
    // Event
  }

The following example updates the property value as the user types. In postCreate, connect onKeyDown and onKeyUp on the text field when intermediateChanges is true.

postCreate: function () 
  {
    // summary : Connects keyboard events of the email text field to update the value of the editor.
    // tags    : protected
   
    if (this.intermediateChanges) 
      {
        this.connect(this.email, 'onkeydown', this._onIntermediateChange);
        this.connect(this.email, 'onkeyup', this._onIntermediateChange);
      }
  },
    
_onIntermediateChange: function (event) 
  {
    // summary : Handles the text field key press events and populates this to the onChange method.
    // tags    : private
    
    if (this.intermediateChanges) 
      {
        this._set('value', event.target.value);
        this.onChange(this.value);
      }
  }

Set focus when the widget loads by implementing the focus method:

focus: function () 
  {
    // summary : Put focus on this widget.
    // tags    : public
    
    dijit.focus(this.email);
  }

For a complete example, see the StringList editor widget in the Alloy MVC template.

To register the custom editor with a property, see Register a custom editor.

Editor widget properties

The following properties control widget behavior. The editor framework or an EditorDescriptor sets them:

  • intermediateChanges – Indicates whether onChange fires for each value change or only on demand.
  • label – Title of the property to be edited.
  • value – Value of the widget.
  • required – Indicates whether the widget requires a value.
📘

Note

Add properties for a particular widget by using a property EditorDescriptor.

Editor widget methods

The following methods handle value changes, focus, and validation:

  • onChange – Fires from within the widget when the value changes. The wrapper listens to this event and updates the property display.
  • focus – Fires when the widget displays. Place focus on the first interactive element.
  • isValid – Runs during validation when an item saves. Returns true if the current value is valid.

Validation

Validation ensures property values meet defined constraints before content saves.

To support validation, implement the isValid method on the widget.

The framework mixes property constraints into the widget at construction time. For example, if the required checkbox is selected in the admin page, the framework passes that constraint as the required property.

isValid: function () 
  {
    // summary : Indicates whether the current value is valid.
    // tags    : public
    
    var emailRegex = '[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+';
    if (!this.required) 
      {
        emailRegex = '(' + emailRegex + ')?';
      }
    var regex = new RegExp('^' + emailRegex + '$');
    return regex.test(this.value);
  }

To add a custom error message during validation, implement getErrorMessage:

errorMessage: "",
    isValid: function () {
        // summary : Indicates whether the current value is valid.
        // tags    : public
                        
        if (this.value.length < 10) {
            this.errorMessage = "email should have at least 10 characters";
            return false;
        }                    
        if (this.value.indexOf("optimizely.com") < 0) {
            this.errorMessage = "email should come from optimizely.com";
            return false;
        }
        var emailRegex = "[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+";
        if (!this.required) {
            emailRegex = "(" + emailRegex + ")?";
        }
        var regex = new RegExp("^" + emailRegex + "$");
        return regex.test(this.value);
    },
    getErrorMessage: function () {
       return this.errorMessage;
    }

Manage child dialog boxes

_HasChildDialogMixin prevents the main dialog from closing when a child dialog opens. This preserves the editing context during multi-dialog workflows.

If the widget launches a dialog box, extend it with epi-cms/widget/_HasChildDialogMixin. Set property values at the right point so the blur event does not close the main dialog.

The mixin provides an isShowingChildDialog property. The main dialog checks this value during its blur event to decide whether to hide. Set isShowingChildDialog to true before opening the child dialog, then reset it to false after the child dialog closes.

_showChildDialog: function () {
    var dialog = dijit.Dialog({
      title: 'Child Dialog'
    });
    this.connect(dialog, 'onHide', this._onHide);
    this.isShowingChildDialog = true;
    dialog.show();
  },

  _onHide: function () {
    this.isShowingChildDialog = false;
  }

Use the custom editor

Apply the custom editor to a property with the ClientEditor attribute:

[ClientEditor(ClientEditingClass = "alloy/component/EmailEditor")]
    public virtual string Email { get; set; }

Or create an EditorDescriptor:

[EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "Email")]
public class EmailEditorDescriptor : EditorDescriptor
{
    public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
    {
        ClientEditingClass = "alloy/component/EmailEditor";

        base.ModifyMetadata(metadata, attributes);
    }
}
📘

Note

If you omit the UIHint, the EditorDescriptor applies to all properties matching the TargetType. To limit it to specific properties, set a UIHint on both the EditorDescriptorRegistration and the property.

ES6 module editor

ES6 module editors offer an alternative to Dojo-based widgets. Build custom property editors with vanilla JavaScript, React, Vue, or other frameworks and standard tooling.

Editor function signature

All ES6 module editors follow a standard function signature with five parameters:

export default function customEditor(
    editorContainer,    // HTML element for rendering the editor
    initialValue,       // Initial property value
    onEditorValueChange, // Callback function notifying CMS of value changes
    widgetSettings,     // Server-provided configuration (optional)
    readOnly           // Boolean indicating editor state
) {
    // Editor implementation
}

Implementation approaches

Simple approach (direct rendering) – Renders into the container without returning a value:

export default function customEditor(editorContainer, initialValue, onEditorValueChange) {
    const input = document.createElement("input");
    input.type = "text";
    input.value = initialValue || "";
    input.onchange = (event) => onEditorValueChange(event.target.value);
    editorContainer.appendChild(input);
}

Advanced approach (lifecycle methods) – Returns an object with lifecycle methods that support CMS features such as undo and redo:

export default function customEditor(editorContainer, initialValue, onEditorValueChange) {
    return {
        render: function () {
            const input = document.createElement("input");
            input.type = "text";
            input.value = initialValue || "";
            input.onchange = (event) => onEditorValueChange(event.target.value);
            editorContainer.appendChild(input);
            this._input = input;
        },
        updateValue: function (value) {
            this._input.value = value;
        },
        destroy: function () {
            // Cleanup event listeners and prevent memory leaks
        }
    };
}

Register the ES6 module editor

Use the ClientEditor attribute with the IsJavascriptModule = true flag:

[ClientEditor(
    ClientEditingClass = "ClientResources/Scripts/Editors/custom-editor.js", 
    IsJavascriptModule = true)]
public virtual string MyProperty { get; set; }
📘

Note

ES6 module editors support TypeScript, React, Vue, Angular, Vite builds, and Optimizely Axiom design system integration. For examples of complex editors, server configuration with EditorDescriptor, and React integration, see Custom Property Editors in Optimizely CMS 13.