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.
NoteUse 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>'
})
}
);
NoteThe 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 whetheronChangefires 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.
NoteAdd 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. Returnstrueif 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);
}
}
NoteIf you omit the
UIHint, theEditorDescriptorapplies to all properties matching theTargetType. To limit it to specific properties, set aUIHinton both theEditorDescriptorRegistrationand 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; }
NoteES6 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.
Updated 17 days ago
