Optimizely CMS 12 provides a rich set of built-in property editors, but sometimes your content model requires a custom editing experience. The CMS editor UI is built on the Dojo Toolkit, and understanding how to create custom Dojo widgets unlocks powerful customization possibilities.
In this guide, you'll learn how to create a custom property editor with a single Dojo TextBox from scratch — including the three real-world 404 errors we encountered and how to solve them.
Step 1: Create the Module Structure
In Optimizely CMS 12, custom Dojo modules must live inside modules/_protected/. Create this folder structure in your web project:
CmsIv.Web/
modules/_protected/
CmsIv.Web/
module.config
Scripts/
Editors/
SimpleTextBoxEditor.js
Create the module.config file at CmsIv.Web/modules/_protected/CmsIv.Web/module.config:
<?xml version="1.0" encoding="utf-8"?>
<module>
<dojo>
<paths>
<add name="app" path="Scripts" />
</paths>
</dojo>
</module>
This registers a Dojo path named app that maps to the Scripts folder inside your module.
Step 2: Create the Dojo Widget
Create the widget at CmsIv.Web/modules/_protected/CmsIv.Web/Scripts/Editors/SimpleTextBoxEditor.js. This JavaScript file defines the behavior of your editor in the browser.
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin",
"dijit/form/TextBox",
"epi/shell/widget/_ValueRequiredMixin"
], function (
declare, lang, _Widget, _TemplatedMixin,
_WidgetsInTemplateMixin, TextBox, _ValueRequiredMixin
) {
return declare("app/Editors/SimpleTextBoxEditor",
[_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _ValueRequiredMixin], {
templateString:
'<div class="dijitInline">' +
'<div data-dojo-type="dijit/form/TextBox" ' +
'data-dojo-attach-point="textBox" ' +
'data-dojo-attach-event="onChange:_onTextBoxChanged" ' +
'style="width: 100%;">' +
'</div>' +
'</div>',
value: null,
_setReadOnlyAttr: function (value) {
this._set("readOnly", value);
if (this.textBox) this.textBox.set("readOnly", value);
},
_setValueAttr: function (value) {
this._set("value", value);
if (this.textBox) this.textBox.set("value", value || "");
},
_getValueAttr: function () {
return this.textBox ? this.textBox.get("value") : this.value;
},
_onTextBoxChanged: function (newValue) {
this._set("value", newValue);
this.onChange(newValue);
},
onChange: function (value) { /* CMS hooks into this */ }
});
});
Step 3: Create the EditorDescriptor
The EditorDescriptor tells Optimizely which Dojo widget to use for a specific UIHint.
using EPiServer.Shell.ObjectEditing;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;
namespace CmsIv.Web.Business.EditorDescriptors
{
[EditorDescriptorRegistration(TargetType = typeof(string), UIHint = UIHintValue)]
public class SimpleTextBoxEditorDescriptor : EditorDescriptor
{
public const string UIHintValue = "SimpleTextBox";
public SimpleTextBoxEditorDescriptor()
{
ClientEditingClass = "app/Editors/SimpleTextBoxEditor";
}
}
}
Step 4: Register the Module and Use the Property
In Optimizely CMS 12 (ASP.NET Core), you must explicitly register the protected module in Startup.cs:
using EPiServer.Shell.Modules;
// Inside ConfigureServices()
services.Configure<ProtectedModuleOptions>(options =>
{
if (!options.Items.Any(m => m.Name == "CmsIv.Web"))
{
options.Items.Add(new ModuleDetails { Name = "CmsIv.Web" });
}
});
Finally, apply the UIHint to your content model:
[UIHint("SimpleTextBox")]
public virtual string CustomNote { get; set; }
Strategic Insights: Troubleshooting 404 Errors
Error 1: Doubled ClientResources Path
Symptom: Browser requests /ClientResources/ClientResources/Scripts/...
Cause: Placing module.config at the web project root with path="ClientResources/Scripts". In CMS 12, the system may automatically prepend path segments depending on location.
Fix: Always place module.config inside modules/_protected/{ModuleName}/ and use a relative path like path="Scripts".
Error 2: Fallback to Shell Module
Symptom: Browser requests /EPiServer/Shell/12.x/ClientResources/Editors/...
Cause: The CMS doesn't recognize your custom module, so Dojo defaults to the Shell's base path. This usually means the module registration in Startup.cs is missing or the module name mismatches.
Error 3: The "ClientResources" Folder Pitfall
Symptom: 404 error even with the correct module name in the URL.
Cause: For protected modules, the Dojo path in module.config maps directly to a folder at the module root. If you follow the CMS 11 pattern of nesting under a ClientResources subfolder without updating the config, it will fail.
Fix: Place files at modules/_protected/{ModuleName}/Scripts/ to match path="Scripts".
Conclusion
Creating custom Dojo property editors requires a precise handshake between C# descriptors and JavaScript AMD modules. By following the _protected folder convention and ensuring ProtectedModuleOptions registration, you can extend the CMS UI reliably. Always monitor the browser's Network tab to verify that your script paths are resolving as expected.