Last updated: Feb 20 2018

Area: Episerver CMS Applies to versions: 10 and higher
Other versions:

Creating an editor widget

This topic describes how to extend the Episerver editing user interface through widgets. 

In this topic

Editor widget properties

Episerver page editing is based on the Dojo client JavaScript toolkit. Each property or block that you edit consists of one or more widgets. A widget is a Dojo JavaScript class which creates a user interface. Widget methods are used by the wrapper that displays the widget.

  • intermediateChanges. Indicates whether the onChange method is used for each value change or only on demand.
  • label. Title of the property to be edited.
  • value. Value of the widget.
  • required. Indicates whether this widget requires a value.

Note: You can add properties for a particular widget by using a PropertyEditorDescriptor.

Editor widget methods

  • onChange. Callback event that should be raised from within the widget when the value has changed. The wrapper displaying this widget listens to this event and updates the user interface when it occurs.
  • focus. Called when the widget is displayed, should give focus to the start element in the widget.
  • isValid. Called during validation when an item is saving; true if the current value is valid.

Creating a simple editor widget

The following example shows the basic structure of a widget class and creates a simple widget for entering a valid e-mail address. To create a widget for a dialog editor, you must inherit from the dijit/_Widget class.

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 the dijit/_TemplatedMixin class, which lets you create a template for the user interface of the widget in either a local string or an external HTML file. You can attach to elements in the template, giving you a programmatic reference to that node in the widget, and you also can attach events to event handlers in our widget.

When the widget is created, the initial value is not available. The initial value is set with set('value', value) and can be called multiple times when the editor user interface is loading.  To make sure you update the value in the text box when it is set on the widget, declare a _setValueAttr method:

_setValueAttr: function (value) {
  // summary:
  //    Sets the value of the widget to "value" and updates the value displayed in the textbox.
  // tags:
  //    private

    this._set('value', value);
    this.email.value = this.value || '';
}

The _setValueAttr method also references a variable named email, which is the textbox DOM node; it is automatically assigned to this variable name by the dijit/_Templated class when it parses the data-dojo-attach-point in the template code.

When you need to populate the changes made in the widget, call the onChange method and pass a new value to it. You should call onChange as often as necessary during editing to provide an accurate preview of changes.

_onChange: function (event) {
  // summary:
  //    Handles the textbox change event and populates this to the onChange method.
  // tags:
  //    private

  this._set('value', event.target.value);
  this.onChange(this.value);
}

The _onChange method is a private event handler that is triggered when a change is made on the text box. It is configured in the template using the data-dojo-attach-event syntax. After it has updated the value, it calls the onChange method which in turn causes a page to update.

onChange: function (value) {
    // Event
}

The following example shows how to update the user interface when a user types in it. In the postCreate method, connect to the onKeyDown and onKeyUp events on the text box element if the intermediateChanges property is set to true.

postCreate: function () {
// summary: // Connects keyboard events of the email textbox 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 textbox key press events event and populates this to the onChange method. // tags: // private if (this.intermediateChanges) { this._set('value', event.target.value); this.onChange(this.value); } }

You can also control where you set the focus when the control loads by implementing the focus method.

focus: function () {
  // summary:
  //    Put focus on this widget.
  // tags:
  //    public

  dijit.focus(this.email);
}

Validation

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

The constraints for a property are mixed into the widget when it is constructed. For example, if the value has the required checkbox selected in admin view, then that is passed through as the property required.

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);
}

Dealing with child dialog boxes

If your widget needs to launch a dialog box of its own, extend an additional class with epi-cms/widget/_HasChildDialogMixin, and set a few property values in the correct place to ensure that the blur event, (going from the main dialog box to the new one), does not close the main dialog box.

The class provides one additional property called isShowingChildDialog, which is used in the blur event of the main dialog to determine whether it should hide itself. Therefore, if you want to prevent hiding of the main dialog box when the widget launches a child dialog box, set the value to true before launching the child dialog box, then set it back to false after the child dialog box 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;
}

Using this custom editor

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

or creating an EditorDescriptor:

    [EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "emaildesc")]
    public class EmailDescriptor : EditorDescriptor
    {
        public EmailDescriptor()
        {
            ClientEditingClass = "alloy/component/EmailEditor";
        }
    }
        [UIHint("emaildesc")]
        public virtual string Email { get; set; }

Note: Without the UIHint, all strings are affected.

Comments

  1. define function is not correct.
    You should not pass acme.widget.EmailTextbox as a first argument in the define function (in first row).
    Spend a few hours to find the actuall reason of an error.

  2. There is an error in is valid section var regex = newRegExp('^' + emailRegex + '$'); should be  var regex = new RegExp('^' + emailRegex + '$');

  3. There are no dijit and dojo injections in define function.

Correct versin of custom dojo control:

define([
"dijit",
"dojo",
"dojo/_base/declare",
"dijit/_Widget",
"dijit/_TemplatedMixin",

"dojo/text!./templates/EmailBox.html"
],
function(
dijit,
dojo,
declare,
_Widget,
_TemplatedMixin,
template
) {
return declare("teva.editors.EmailBox",
[
_Widget,
_TemplatedMixin
],
{
_tableNode: null,
templateString: template,
_setValueAttr: function (value) {
// summary:
// Sets the value of the widget to "value" and updates the value displayed in the textbox.
// tags:
// private

this._set('value', value);
this.email.value = this.value || '';
},
_onChange: function (event) {
// summary:
// Handles the textbox change event and populates this to the onChange method.
// tags:
// private

this._set('value', event.target.value);
this.onChange(this.value);
},
postCreate: function () {
// summary:
// Connects keyboard events of the email textbox 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 textbox key press events event and populates this to the onChange method.
// tags:
// private

if (this.intermediateChanges) {
this._set('value', event.target.value);
this.onChange(this.value);
}
},
focus: function () {
// summary:
// Put focus on this widget.
// tags:
// public

dijit.focus(this.email);
},
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);
},

_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;
}
});
});

And do not forget to put 

<div>
    <input type="email" data-dojo-attach-point="email" data-dojo-attach-event="onchange:_onChange" />
</div>

in templates folder

Hi! Thankyou for the findings. I have updated the example

I can't quite figure out where to put the .js file for the widget. 

Hi @BenjaminSorterup it should be located under the ClientResources/component/EmailEditor.js in this case. And the path for it will be alloy/component/EmailEditor

see the Module.config where alloy is mapped to script folder

I'm having an issue where in page-editing view, _setValueAttr() is never called and thus the fields are never initialized.

My code (disregard the startup method I copy pasted it prematurely) is on this other post

@Benjamin You can try to put your .js widget file in ClientResources/Scripts/widgets

how to call all these functions. i tried calling _onChange and get a console error that this.onChange is not a function

@nitinanand you need to implement that method 

onChange: function (value) {
// Event
}

@PeterArsenyev i tested hooking up this _setvalueAttr in our code and its called when you focus the textbox. I saw that you got an working example in our other tread. Good luck with the rest