Håkon Nordli
Oct 20, 2015
  9482
(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
Copy Optimizely SaaS CMS Settings to ENV Format Via Bookmarklet

Do you work with multiple Optimizely SaaS CMS instances? Use a bookmarklet to automatically copy them to your clipboard, ready to paste into your e...

Daniel Isaacs | Dec 22, 2024 | Syndicated blog

Increase timeout for long running SQL queries using SQL addon

Learn how to increase the timeout for long running SQL queries using the SQL addon.

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Overriding the help text for the Name property in Optimizely CMS

I recently received a question about how to override the Help text for the built-in Name property in Optimizely CMS, so I decided to document my...

Tomas Hensrud Gulla | Dec 20, 2024 | Syndicated blog

Resize Images on the Fly with Optimizely DXP's New CDN Feature

With the latest release, you can now resize images on demand using the Content Delivery Network (CDN). This means no more storing multiple versions...

Satata Satez | Dec 19, 2024