Håkon Nordli
Oct 20, 2015
  9392
(10 votes)

Learn how to develop your own Dojo widget

“It was like getting a brick in the head, » a developer told me after a dojo seminar a while back.   Often when I talk to developers about dojo, I get the same message, they have no idea where to start. They find it a bit too difficult and it takes too much time to understand the basics. 

This has led me to write a new blog post on how to build your own Dojo widget. I have decided to split the article in two separate parts, first how and where to start building a widget and second, 10 tips that may come in handy when developing. 

Before we start developing, I would like to point out a few things. Dojo is a library much like jQuery, you may do DOM manipulations and other fun things. Together with Dojo, you will also get Dijit, which is Dojo’s UI library. We use Dijit when dealing with widgets in EPiServer. 

Step by step guide 

Here is a list of files that are necessary for you to build your first widget. Depending on what you would like to develop, you might need other files as well. However, these are the ones you must have. 

Step 1 – EditorDescriptor.cs 

First, you need an EditorDescriptor. I suggest putting it in your Business/EditorDescriptors folder. 

It defines which editor to use for a specific property for a page or shared content. You may read more about it here. http://world.episerver.com/documentation/Items/Developers-Guide/EPiServer-CMS/7/Editing/How-To/Registering-a-Custom-Editor/

using System;
using System.Collections.Generic;
using EPiServer.Shell.ObjectEditing.EditorDescriptors;

namespace Alloy.Business.EditorDescriptors
{
    [EditorDescriptorRegistration(TargetType = typeof(String), UIHint = "NewTextarea")]
    public class NewTextareaEditorDescriptor : EditorDescriptor
    {
        public override void ModifyMetadata(EPiServer.Shell.ObjectEditing.ExtendedMetadata metadata, IEnumerable<Attribute> attributes)
        {
            ClientEditingClass = "alloy/editors/NewTextarea";
            base.ModifyMetadata(metadata, attributes);
        }
    }
}

Step 2 – The dojo widget (JavaScript file)

This is the actual widget where a lot of the magic happens. I suggest putting the file in your ClientResources/Scripts/Editors folder. 

Here is an example of a small widget, a textarea. I recommend starting with this when building a widget for the first time. Later you can rewrite it and make it do whatever you need it to be. This way you have an excellent starting point with no errors.

The reason I have built a textarea is because EPiServer already have a textarea. That way no one will actually use this widget, but instead use it as a starting point. 

The widget is based on StringList from Alloy, but are rewritten and have several comments that explain several of the methods and modules.

define([
    "dojo/_base/array",
    "dojo/query",
    "dojo/on",
    "dojo/_base/declare",
    "dojo/_base/lang",

    "dijit/_CssStateMixin",
    "dijit/_Widget",
    "dijit/_TemplatedMixin",
    "dijit/_WidgetsInTemplateMixin",

    "dijit/form/Textarea",
   
    "epi/epi",
    "epi/shell/widget/_ValueRequiredMixin"
],

function (
    array,
    query,
    on,
    declare,
    lang,

    _CssStateMixin, //_CssStateMixin is a mixin for widgets that set CSS classes on their nodes depending on hover/active/focused state, and also semantic state (checked, selected, disabled, etc.).
    _Widget, //BaseClass for all dijit widgets
    _TemplatedMixin, //dijit/_TemplatedMixin is a mixin for most widgets in dijit. It takes an HTML template, and creates the widget’s DOM tree according to that template.


    _WidgetsInTemplateMixin, //When using this template in a directly extended widget class, you will need to mixin dijit._WidgetsInTemplateMixin in addition to dijit._TemplatedMixin.
   
    Textarea,
   
    epi,
    _ValueRequiredMixin //Extends dijit/_CssStateMixin: http://world.episerver.com/Documentation/Javascript-Library/?documentId=episerverframework/7.1/epi/shell/widget/_ValueRequiredMixin
) {
    return declare("alloy.editors.NewTextarea", [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin, _CssStateMixin, _ValueRequiredMixin], {
        templateString: "<div class=\"dijitInline\">\
                            <div data-dojo-attach-point=\"textArea\" data-dojo-type=\"dijit.form.Textarea\"></div>\
                        </div>",

        intermediateChanges: false, //default false. intermediatechanges is a dijit property which fire the change event when there is an actually change. 
        value: null,
        onChange: function (value) {
            // Event
        },

        //built-in dijit widget. postCreate is run after all the properties are set up, but before the templateString is added to the DOM
        postCreate: function () {

            // call base implementation
            this.inherited(arguments);

            // Init textarea and bind event
            this.textArea.set("intermediateChanges", this.intermediateChanges);
           
            // Notice the lang.hitch. It forces the on method to retain it’s original scope.  
            on(this.textArea, "change", lang.hitch(this, this._onTextAreaChanged));
        },
       
        //built-in dijit widget. Runs after the templateString is added to the DOM. 
        startup: function () {

        },

        // build-in dojo method. Prevents an invalid form from submitting
        isValid: function () {
            return !this.required || lang.isArray(this.value) && this.value.length > 0 && this.value.join() != "";
        },

        // Setter for value property. Runs at startup. 
        _setValueAttr: function (value) {
            this._setValue(value, true);
        },

        //runs at startup. 
        _setReadOnlyAttr: function (value) {
            this._set("readOnly", value);
            this.textArea.set("readOnly", value);
        },

        //A setter for intermediateChanges
        _setIntermediateChangesAttr: function (value) {
            this.textArea.set("intermediateChanges", value);
            this._set("intermediateChanges", value);
        },

        // Event handler for textarea
        _onTextAreaChanged: function (value) {
            this._setValue(value, false);
        },

        _setValue: function (value, updateTextarea) {

            //avoids running this if the widget already is started
            if (this._started && epi.areEqual(this.value, value)) {
                return;
            }

            // set value to this widget (and notify observers). Built-in dijit method. Source: https://www.sitepen.com/blog/2013/10/16/dojo-faq-what-is-the-difference-between-set-and-_set/
            this._set("value", value);

            // set value to textarea
            updateTextarea && this.textArea.set("value", value);

            if (this._started && this.validate()) {
                // Trigger change event
                this.onChange(value);
            }
        },
    });
});

Step 3 - Module.config

Module.config should be in your web root folder. It is to add a mapping from alloy to ~/ClientResources/Scripts to the dojo loader configuration. If you do not have this, you will get 404 errors for your widget file. 

<?xml version="1.0" encoding="utf-8"?>
<module>
    <dojo>
        <!-- Add a mapping from alloy to ~/ClientResources/Scripts to the dojo loader configuration -->
        <paths>
            <add name="alloy" path="Scripts" />
        </paths>
    </dojo>
</module>


Here is EPiServer’s documentation about module.config 
http://world.episerver.com/documentation/items/developers-guide/episerver-framework/7/configuration/configuring-moduleconfig/ 

 

Step 4 – Use the widget on a page

To add the widget to a property, use the UIHint showed below:

[UIHint("NewTextarea")]
[Display(GroupName = SystemTabNames.Content, Order = 10)]
public virtual string NewTextarea { get; set; }

The End

These files are only meant as a starting point. You may use them and rewrite them to whatever you want. 

Remember to update the namespaces. 

Happy coding! 

Oct 20, 2015

Comments

Oct 20, 2015 01:06 PM

Nice! And a lighter brick! Much needed, thanks for sharing.

jukkahyv
jukkahyv Oct 21, 2015 01:29 PM

When programming client side with Angular and Knockout I've been used to separating the view from the logic (viewmodel/controller, however you call it). My main reason for disliking Dojo is that to me it seems that Dojo doesn't support this approach since all the DOM handling is mixed with the JavaScript code. I would like the code to be more testable. Even this article doesn't mention unit testing at all. So my question is: is it possible to break the dependencies to browser and EPi so that you could test this widget without initializing the EPi context?

Håkon Nordli
Håkon Nordli Oct 25, 2015 05:58 PM

@Jukka If you remove the epi and _ValueRequiredMixin modules it should be possible to run this widget outside EPiServer. I've tried it once before with another widget and it worked. Dojo got their own test framework, DOH. I've never used it so I don't know more than that it exists. According to it's documentation it supports non-browser environments. https://dojotoolkit.org/reference-guide/1.9/util/doh.html  

nitin anand
nitin anand Jul 18, 2018 01:29 PM

Hi I am trying to get template from my controller class as  templateString: dojo.cache("/LatestChanges")   The controller is LatestChangesController.cs which returns an Index view in turn. It works in alloy project but not on my actual project. i get error 404 as displayed in image

https://imgur.com/93jlr5A

Any help would be appreciated

Please login to comment.
Latest blogs
I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog

ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results

Introduction In Optimizely CMS, content that is moved to the trash can still appear in search results if it’s not explicitly excluded using the...

Ashish Rasal | Nov 7, 2024

CMS + CMP + Graph integration

We have just released a new package https://nuget.optimizely.com/package/?id=EPiServer.Cms.WelcomeIntegration.Graph which changes the way CMS fetch...

Bartosz Sekula | Nov 5, 2024