September 6, 2017

Building an Episerver Component with Dojo including Localization and Context

I'm rather torn about whether or not to break this up into multiple, topical posts as it threatens to be a bit long. We'll see how it goes... (ended up being one very long post)

I recently released a new "Page Preview" package for Episerver 10+ that adds a (somewhat large and obvious) Page Preview button to the author components / gadgets. The add-on is documented in the Git repo linked. This article (or series) is more about creating such a gadget - I'm going to keep it as high-level and logical as I can.

My add-on uses a Partial Router to function, which is merely utilized by the functionality the add-on is meant to provide, not core to the fact that it is a component. So though that is in my code, it's not covered here. I do cover building a partial router in the docs I wrote for my Episerver Ascend 2017 presentation and the topic is definitely worth a Google if you've a mind to check it out.

TL/DR

  1. Documentation is sparse, so expect to piece a lot together. It's out there.
  2. Lots of links below. Use them.
  3. Dojo is a pain, but powerful.
  4. There are a lot of parts that are required to make this work as if by magic.

The C# Code

There's really very little in the way of .NET / C# code when it comes to the back-end code for creating a component. My code here is slightly customized - only to provide translation capabilities, which aren't part of Episerver's OOTB Component attribute.

The properties below are all standard, I merely overrode the Title and Description properties, which are user-facing, in order to be able to translate them.

using EPiServer.Shell;

namespace eGandalf.Epi.PagePreview
{
    [LocalizedComponent(
        PlugInAreas = PlugInArea.Assets, 
        Categories = "cms", 
        WidgetType = "egandalf/PagePreview", 
        Title = "/egandalf/pagepreview/componentTitle", 
        SortOrder = 1000,
        Description = "/egandalf/pagepreview/componentDescription"
        )]
    public class ComponentDefinition
    {
    }
}

PluginAreas:
  • Assets - allows the component to be placed into one of the Assets panels to the left or right of the authoring interface.
  • AssetsDefaultGroup - adds the component as a tab to the default assets area (Blocks, Media, etc.)
  • Navigation / NavigationDefaultGroup - allows the component to be placed into the main navigation of the platform.
Categories (does not have any const values from which to select, so must be a static string), yet it is limited in the choices that will do any good. Mainly, this is to categorize the items for the author browsing add-ons for the assets panel. Options are: "cms", "content", or "dashboard" if your add-on is a dashboard component.

For more information about Episerver Components and most notably recent changes to how they should be written, see The Old Gadget Framework is Deprecated by Grzegorz Wiecheć.

The WidgetType parameter should be the namespace / path for your Dojo widget. In my case, I'm just using a relatively simple path. In my case above "egandalf/PagePreview", "egandalf" is the namespace for my dojo widget and "PagePreview" is the name of the widget itself - which is also the file name for the *.js file containing the widget code, relative to the ClientResources folder inside my module's zipped contents.

From what I've read, if you want your component(s) in sub directories, simply add those to the WidgetType string (which will need replicated elsewhere, as we'll see). E.g. /egandalf/widgets/PagePreview.

NOTE: In some older online samples, this uses a "dot" notation instead of path. The dot notation is unsupported in recent versions of Episerver CMS and you should use path notation as above instead.

For the C# code, that's more or less it. There's really not much aside from making Episerver aware of the component. But then you get into the land of Dojo and, there, my friends, there be monsters.

The Dojo Devil

It's a rather unfortunate reality that Episerver's client side tools, modules, and mix-ins for Dojo are poorly documented. This is sad because this area is ripe with potential for modifications and add-ons to enhance the author experience. Thankfully, Episerver also has a great community and, through their helpful contributions, I was able to piece this together.

Before I begin attempting to describe my own code, you may want to brush up on Dojo to at least gain an understanding of it. My own impression, for what it's worth, is that its biggest advantage is that it is incredibly, incredibly modular. However, that also is its biggest setback. To a friend, I likened it to taking every function you write for an application, building an interface around it, then setting up a service architecture in which every service for every function has to be specified before it may be used. Overkill? Perhaps. Hyperbole? Not as much as you might hope.

I'm not really a stranger to JavaScript. I've mucked about in it off and on since the days when JQuery was the new hotness. I've done a little pure JS, a little Angular, a little Node, and a bit more. So I'm accustomed to picking up new frameworks. This one was a pain in the ass not because Episerver's documentation wasn't great, but also because Dojo's own documentation and community at large aren't altogether impressive either.

So in the spirit of doing the Google for you, here are some links you may want to check out. There may be overlap in the links as the first one below is also an aggregate of helpful links. I only found it afterward and am including it anyway. All of the other links I used to some degree.

Episerver:
That's a lot of links. I got a little something from each one, though for some I might struggle to point out exactly what. Sometimes it was simply a greater understanding for how things worked. Regardless, it took a lot of reading to understand what the hell I was doing. I'm still not sure I do.

My Widget Code

With that, let's take a look at how much code came out of all that reading...

define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/html",
    "dijit/_TemplatedMixin",
    "dijit/_WidgetBase",
    "epi-cms/_ContentContextMixin",
    "epi/i18n!epi/cms/nls/egandalf.pagepreview",
    "dojo/text!./templates/PreviewControl.html"
],
    function (declare, lang, html, _TemplatedMixin, _WidgetBase, _ContentContextMixin, resources, template) {
        return declare("egandalf/PagePreview", [_WidgetBase, _TemplatedMixin, _ContentContextMixin], {
            templateString: template,
            postCreate: function () {
                var self = this;
                dojo.when(this.getCurrentContext(), function (context) {
                    self.refreshByContext(context);
                });
            },
            contextChanged: function (context, callerData) {
                this.refreshByContext(context);
            },
            refreshByContext: function (context) {
                if (context !== undefined && context.capabilities !== undefined && context.capabilities.isPage == true) {
                    html.set(this.previewHeader, resources["header"]);
                    html.set(this.previewInstructions, resources["instructions"]);
                    html.set(this.previewLink, '' + resources["button"] + '');
                } else {
                    html.set(this.previewHeader, resources["errorheader"]);
                    html.set(this.previewInstructions, resources["errormessage"]);
                    html.set(this.previewLink, '');
                }
            }
        });
    }
);

That's 36 lines of pure awesome.

I'm going to be honest, this went through a lot of revisions, a lot of testing, and a lot of inspecting to get to just this point. Part of the reason is that things seemed to fail silently far too often. If I was in edit mode trying to load my component only to see nothing happening, there was all too frequently no messages or errors logged in the console in my browser. I set breakpoints, which helped, but due to the nature of the code - where most of it is in a massive "declare" function as part of the return, most of the breakpoints would fail. Trying to hit a breakpoint early and then step into the code was equally useless as it would step into function after function after function in the Dojo source.

That being said, I'm going to walk through this code as best I can and attempt - ATTEMPT - to explain what's going on.

From the top!

define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/html",
    "dijit/_TemplatedMixin",
    "dijit/_WidgetBase",
    "epi-cms/_ContentContextMixin",
    "epi/i18n!epi/cms/nls/egandalf.pagepreview",
    "dojo/text!./templates/PreviewControl.html"
],

What I'm doing here is telling Dojo which dependencies need to be resolved for my code to function correctly.

I'm returning a "declare" as Dojo widgets do, so that's necessary. Skip down a couple to the "html" dependency, used to set values in my template, which leads me to... "dijit/_TemplatedMixin" which allows me have a template on which to apply changes. I'll show you this template in a bit, but like many JS frameworks it includes points for data-binding values in the widget to the markup. "_WidgetBase" is a base dijit for any widgets you want to develop - I didn't go into what exactly it does. The last item on the list - "dojo/text..." - loads up my template from another file. Everything after the ! is essentially the parameter used by the dependency to load up additional information. In this case, loading the template markup as a string using the "./" notation to denote that the path is relative to the widget. Without that last I'd have to manage the template markup as a string inside of this JS file, which I find abhorrent.

As I read through the list, I can probably get rid of the "lang" dependency as it appears I'm no longer using it. A sacrifice to the gods of code revision.

This leads me to the final two to be mentioned: "epi-cms/_ContentContextMixin" and "epi/i18n...".

To put it simply, these two items give me access to the current author editing context and Episerver's localization service, respectively.

For the localization dependency, the information after the ! in that line tells it which translation node to load. "epi/cms/nls/" tells it to use Episerver's translation data and "egandalf.pagepreview" is a dot-notation representation of the XPath to my translation values. As can be seen here:

<languages>
  <language id="en" name="English">
    <egandalf>
      <pagepreview>
        <!-- value nodes here -->
      </pagepreview>
    </egandalf>
  </language>
</languages>

So that takes care of my dependencies. As simply taking care of dependencies accounts for some 25% of the total lines of JS, you may start to understand what I was getting at when complaining about how insanely modular this framework is.

Anyway, then we define the function in which all of the logic will occur. You'll note that the function parameters provides a mirror of the dependencies. Dojo takes the defined dependencies, loads them, then calls the function with each dependency represented as a parameter. Note that order matters.

function (declare, lang, html, _TemplatedMixin, _WidgetBase, _ContentContextMixin, resources, template) {

My return statement declares the widget I'm creating. Note that the first parameter here is the same as the value for the WidgetType parameter in my component definition in the C# code above. We'll see this again in one other place later.

return declare("egandalf/PagePreview", [_WidgetBase, _TemplatedMixin, _ContentContextMixin], {

Now, finally, we're getting into the meat and bones of the widget.

The first three properties I'm passing into my declare function are given to me by the mix-ins I've included.

templateString: template,
postCreate: function () {
    var self = this;
    dojo.when(this.getCurrentContext(), function (context) {
        self.refreshByContext(context);
    });
},
contextChanged: function (context, callerData) {
    this.refreshByContext(context);
},

templateString takes the template markup loaded as a dependency (from an external file) and assigns it to the widget so that it knows where to data-bind values later. I'll include the template later.

The next two - postCreate and contextChanged - are lifecycle events (see Dijit Lifecycle Methods link above). In each, I eventually make a call to my custom "refresh" method, but it's important in this add-on to have both of these events.

I started by having all necessary logic inside the contextChanged method, but encountered an issue where the Preview button wouldn't load when entering edit mode initially, but would load if I navigated to editing another page. Obviously, initial load of the editor doesn't necessarily constitute a "contextChanged" event (I might argue otherwise... to go from non–editing to editing is a change). Even if it worked, it did not seem to be reliable, however.

So I used the postCreate event. This occurs after the widget is otherwise initialized, so after the constructor. However, using it directly also sometimes failed to load the button. I ended up using "dojo.when" which appears to be Dojo's implementation of a Promise in which the second passed method will fire after the first passed method completes. Make sure to keep scope in mind (note the self = this) as the inner function won't have any contextual awareness of the outer function and "this" therefore takes on a new meaning.

In effect, this is telling Dojo to fire the second function - a call to my own logic - only after the Episerver context has been established. Using a promise like this solved the problem of the widget not loading upon initial entry into edit mode. However, it would only work for the initial context. Navigating from page to page in edit mode would not reset the button, which is where the contextChanged event comes in. Again, calling my own logic whenever the context changes.

refreshByContext: function (context) {
    if (context !== undefined && context.capabilities !== undefined && context.capabilities.isPage == true) {
        html.set(this.previewHeader, resources["header"]);
        html.set(this.previewInstructions, resources["instructions"]);
        html.set(this.previewLink, '<a class="eg-btn" href="' + context["publicUrl"] + context["id"] + '" target="_blank">' + resources["button"] + '</a>');
    } else {
        html.set(this.previewHeader, resources["errorheader"]);
        html.set(this.previewInstructions, resources["errormessage"]);
        html.set(this.previewLink, '');
    }
}

By adding this function adjacent to the built-in widget events, I am able to keep it scoped to each instance of each widget, which is great. You really can't say this enough in JavaScript: Keep. Scope. In. Mind.

Yes, I could have added it globally. However, multiple instances of the widget then would have loaded the method multiple times. In addition, putting it into the global scope would also bring about the possibility of conflicts if any other function were defined in the same scope with the same name. Whatever I do here, I want to make sure what I'm doing is isolated from the rest of the application.

The conditional is checking to make sure the conditions are correct to use the Preview function. I have context, it has capabilities, and one of those capabilities says that the current context is a Page (can't preview Blocks...yet).

If conditions are met, then I'm using the html dependency to set values in the templated markup. Something I found very odd about this is that the html dependency can't do anything with the DOM of the template, such as add a node as an object or add or set attribute values. For that you need other dependencies...because reasons. Rather, the html dependency takes care of data-binding information via html.set().

Values like "previewHeader" and "previewLink" are set inside the template and exposed here. These are my anchor points for data-binding.

The Episerver language dependency gives me access to language resources from my localization XML. In this case, "resources" represents the <pagepreview> node (as defined in the dependencies at the top) and the strings used as the key are the leaf nodes in that XML in which translated values are stored.

Finally, the context parameter and keys is giving me access to context information about the current object being edited such as publicUrl (boy that was a handy find) and id, the latter of which represents the work id for the current draft (MAJOR_minor).

If the context doesn't match conditions for a preview, I'm using the same template and applying other messaging. You'll see these values when, for example, editing a Block.

Template

For reference, here is the template I'm using:

<div class="eg-preview">
    <h1 data-dojo-attach-point="previewHeader"></h1>
    <p data-dojo-attach-point="previewInstructions"></p>
    <div data-dojo-attach-point="previewLink"></div>
</div>

The "data-dojo-attach-point" values are setting up the anchor points for data-binding values from the widget. Note the names matching properties in the widget code.

Module.config

Whew. Nearly to the end.

For documentation about the Module.config, see the links above.

What we need now is something that let's Episerver know about our Dojo widget and really ties together some of the loose ends. For example, I've said nothing so far about styling the widget, though you'll have undoubtedly noticed the "eg-" prefixed classes in my template above.

The module config for this project contains four sections: assemblies, dojo, clientResources, and requiredResources.

Rather than take each in turn, I'll let Episerver's documentation (link above) handle a little. I'm going to address a couple of items, though.

<?xml version="1.0" encoding="utf-8"?>
<module loadFromBin="false" description="Allows page preview in view mode." tags=" EPiServerModulePackage " clientResourceRelativePath="">
  <assemblies>
    <add assembly="eGandalf.Epi.PagePreview" />
  </assemblies>
  <dojo>
    <paths>
      <add name="egandalf" path="ClientResources"/>
    </paths>
  </dojo>
  <clientResources>
    <add location="preview-styles" path="ClientResources/css/styles.css" resourceType="Style" isMinified="false"/>
  </clientResources>
  <clientModule>
    <moduleDependencies>
    </moduleDependencies>
    <requiredResources>
      <add name="preview-styles"/>
    </requiredResources>
  </clientModule>
</module>

First, the <dojo> section. Here I'm basically adding a reference path to the location of my top-level JavaScript namespace. When you see "egandalf/PagePreview" elsewhere, this entry is telling the system that "egandalf" points to the "ClientResources" folder for this add-on (inside the zip).

The <clientResources> section defines the resources that I want to use for my component. This supports only Style or Script resourceType values. The location value is what I want to call this resource and it's worth noting that the path value is relative to the component root (zip file), not relative to the ClientResources folder, as some other posts will state.

Finally, the <requiredResources> node tells the platform that I consider the CSS to be a required item so that it gets loaded with the component. I don't have to do anything else such as include a reference to the stylesheet inside of the template. I do question, however, whether there's a more appropriate approach to scoping my CSS rules so as to not interfere with anything else in the platform.

As an FYI, you can give multiple resources in the <clientResources> section the same "location" value so you would only have to have a single entry in <requiredResources> to include several items. Sort of like bundling, though I'm not sure that doing so will actually merge or minify the files.

Best of luck and happy coding!

No comments: