---
title: Create an editor widget
slug: cms-13/docs/create-an-editor-widget
docTags: 
createdAt: 2026-07-30T20:22:11.023Z
---

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.

:::hint{type="info"}
**Note**
Use the [Optimizely eslint-plugin-cms tool](https://www.npmjs.com/package/@episerver/eslint-plugin-cms) to verify that only public non-deprecated Optimizely Content Management System (CMS) APIs are used when creating widgets.
:::

```javascript
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>'
        })
    }
);
```

:::hint{type="info"}
**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:

```javascript
_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.

```javascript
_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.

```javascript
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`.

```javascript
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:

```javascript
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`](https://github.com/episerver/alloy-mvc-template/blob/master/src/Alloy.Mvc.Template/ClientResources/Scripts/Editors/StringList.js).

To register the custom editor with a property, see [Register a custom editor](docId\:FSs5y5aZhvrr4crA88pLW).

## 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.

:::hint{type="info"}
**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.

```javascript
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`:

```javascript
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.

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

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

Or create an `EditorDescriptor`:

```csharp
[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);
    }
}
```

:::hint{type="info"}
**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:

```javascript
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:

```javascript
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:

```javascript
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:

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

:::hint{type="info"}
**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](https://world.optimizely.com/blogs/grzegorz-wiechec/dates/2026/3/custom-property-editors-in-optimizely-cms-13/).
:::
