Pages

Thursday, October 11, 2018

Replacing HTML strings with Dojo TemplatedMixin

I've been playing around with Dojo a bit recently, modifying some Episerver widgets, and extending functionality, and I ran into a scenario I wasn't really satisfied with. In one situation I encountered, I needed to supply an HTML string for a widget to utilize when rendering data for an object, but the HTML markup had some conditions to it based on the object I was on at the time. Another situation involved looping through items and adding them to a list for display in a widget, again needing to include data from the object in the markup.

I searched online for how to parse HTML strings to include object data or conditions in it, but I didn't find the kind of answer I wanted. Many of the examples online show HTML strings in the Dojo script using concatenation for adding the variables, and others involved multiple lines of node creation and dom manipulation using "dojo.place" and "domConstruct". If you want easily adjustable markup using a template approach, avoiding modifying your dojo JS, these techniques are not the most user friendly. However, I did discover a solution.

What I didn't like

As I mentioned, I found a various SlackOverflow questions and Dojo articles explaining how to supply HTML to various widgets. I've included links to some of these articles or questions at the bottom of the article, but the short of it is, as said earlier, many of them use dom manipulating methods, or strings of HTML in the Dojo JS. 

In my opinion, "dojo.place" and "domConstruct" have their place, but using them to construct large blocks of markup is not ideal. Additionally, including 5-10 lines of HTML in a JS file is not easily manageable or ideal as well. You can clean up the HTML in JS a bit, where string concatenation is used, by switching the strings to ES6 Template Literals, but that still doesn't get the HTML out of the JS.
[pre class="brush:csharp;" title="From Dojo Documentation - dojo.create"]
// dojo 1.7+ (AMD)
require(["dojo/dom-construct", "dojo/_base/array"], function (domConstruct, arrayUtil) {
    var ul = domConstruct.create("ul", null, "someId", "first");
    var items = ["one", "two", "three", "four"];
    arrayUtil.forEach(items, function (data) {
        domConstruct.create("li", { innerHTML: data }, ul);
    });
});

// dojo < 1.7
var ul = dojo.create("ul", null, "someId", "first");
var items = ["one", "two", "three", "four"];
dojo.forEach(items, function (data) {
    dojo.create("li", { innerHTML: data }, ul);
});
[/pre][pre class="brush:csharp;" title="Also Dojo Documentation - html.set"]
html.set(dom.byId("content"), '<tbody>'
    + '<tr>'
    + '<td><label for="amt">How Much?</label></td>'
    + '<td><input type="text" id="amt" name="amt" data-dojo-type="dijit/form/NumberTextBox" value="0"'
    + ' data-dojo-props="constraints: { min: 0, max: 20, places: 0 },'
    + ' promptMessage: \'Enter a value between 0 and +20\','
    + ' required: true,'
    + ' invalidMessage: \'Wrong!\'" />'
    + '</td>'
    + '</tr></tbody>',
    {
        parseContent: true
    }
);
[/pre]

My problem is, I don't want someone messing with the Dojo script just to tweak some HTML elements. I'd prefer to have some sort of template with data binding, like the TemplatedMixin supports, but you cannot have more than one template for a widget. 

You can, however, nest widgets.

A Different Idea

There are a couple ways I found to make this work. Both approaches leverage the TemplatedMixin and involve making smaller, nested widgets. These are rather simple techniques that I haven't seen mentioned much in the articles and posts I found, and I think they make templating and markup in Dojo easier to manage.

Approach 1: Just a Widget

In the case where I needed to add list items to a widget, I decided to make a ListItem widget in my code to reuse for each item in my for-loop. I've used it with more complex objects, but for this example I put together a simple item object that has 3 properties: name, subTitle, href. I also wrote a small function to throw together an array of sample "item" objects to demonstrate this approach.
[pre class="brush:javascript"]
_getItemsArray: function (maxCount) {
    var items = [];
    for (var i = 1; i <= maxCount; i++) {  // 1 based index for visual purposes
        items.push({
            name: `Test item ${i}`,
            subTitle: `Subtext for item #${i}`,
            href: (i % 3 === 0 ? `http://www.mysite.com/item${i}` : '') // every 3rd items gets a link - why not?
        });
    }
    return items;
}
[/pre]
I then made a template for these items by creating an HTML file the same as I would for any widget. For the real functionality, I needed slightly different markup for items that had a hyperlink (href property), so I needed to have conditional markup. Unfortunately, the standard Dojo template engine doesn't permit conditional blocks in your template markup. Fortunately, Dojo has another templating engine using Django Template Language (DTL), and this does support conditions, with slightly different markup for data binding. An example of a template like this is below. Ignore the inline styles, as this is just an example.
[pre class="brush:xhtml"]
<li style="border-bottom: 1px solid #ffc871;">
    <h3 style="color: #5ece43; margin: 2px 0;">
    {% if item.href != "" %}
    <a href="{{item.href}}" style="color: #ff592c; font-weight: bold;">{{item.name}}</a>
    {% else %}
    {{item.name}}
    {% endif %}
    </h3>
    <p style="margin: 2px 0 5px;">{{item.subTitle}}</p>
</li>
[/pre]
As you can see, it's a bit different than your usual Dojo template. Conditions are marked with a bracket percent syntax "{% code %}" and variables are referenced using a double bracket syntax "{{ myVar }}".

To use this template approach you must include the DTL template module in your widget. I have both in my Episerver component I built for this so my defined modules look like this:
[pre class="brush:javascript"]
define([
    // core dojo modules
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/dom",
    "dojo/dom-construct",
    "dojo/on",
    "dojo/request",
    // widget template modules
    "dijit/_Widget",
    "dijit/_TemplatedMixin",    // standard dojo template lang
    "dojox/dtl/_Templated", // dojo template lang - supports conditionals with
    "dojox/dtl/tag/logic",      // this, the module needed for those conditionals
    // epi component modules
    "epi/epi",
    "epi/shell/widget/command/RemoveGadget",
    "epi-cms/widget/_HasChildDialogMixin",
    "epi-cms/widget/ContentSelector",
    // templates
    "dojo/text!./templates/componentTemplate.html",  // widget template
    "dojo/text!./templates/listItemTemplate.html",  // sample template for listItems
    "dojo/text!./templates/markupBlockTemplate.html",   // sample template for markup string
],
    function (
        declare,
        lang,
        dom,
        domConstruct,
        on,
        request,

        _Widget,
        _TemplatedMixin,
        _DtlTemplatedMixin,
        _dtlLogic,

        epi,
        RemoveGadget,
        _HasChildDialog,
        ContentSelector,

        _template,
        _itemsTemplate,
        _markupBlockTemplate
    ) {
        /* there is stuff in here */
    }
);
[/pre]
To utilize the templating, a widget needs to include the templated mixin, and in the code below it leverages the DTL mixin. The widget doesn't need to be complicated, but it should have properties defined matching the binding in the template. Referring to my sample ListItem template above, you can see an "item" object with different properties.
[pre class="brush:javascript"]
    var ListItem = declare([_Widget, _DtlTemplatedMixin], {
        templateString: _itemsTemplate,
        // item object for template
        item: {}
    });
[/pre]
Then, any time a new List Item is needed, a new instance of the ListItem widget can be created, with the "item" set, and the template will bind the values. Using my sample items array, and a UL node defined as a Dojo attach point named "itemsList" in my main widget template, the list can be created like the following:
[pre class="brush:javascript"]
    createList: function () {
        var itemsArray = this._getItemsArray(8); // for sample
        for (var i = 0; i < itemsArray.length; i++) {
            var newItem = new ListItem({
                item: lang.mixin({ index: i }, itemsArray[i]) // i'm not actually using the index property
            });
            newItem.placeAt(this.itemsList);
        }
    }
[/pre]
This is the resulting view from my Episerver component.

Approach 2: Widget Markup

In some cases you need an HTML markup string instead of a widget. I have encountered that in a couple cases, and the latest example was the Dojo DGrid column formatter. The formatter function is expected to return a string of HTML for rendering data in the column. Putting a block of HTML in the JS to accomplish this is messy, though, and makes changes risky.

This technique builds off the approach above, and instead of attaching the widget to your main widget markup, you simply leverage the "domNode.innerHTML" property. I created a different sample template, like a bio block for a person, to demonstrate this. The same parent widget created above is used for this example, so the module definitions are the same, but the template for this example is less complex, as shown.
[pre class="brush:javascript"]
<div>
    <div>
        <h4>${fullName}</h4>
        <p>
            <b>BIO:</b>&nbsp;${fullName} works as a ${occupation} and
            can be contacted by emailing to ${emailAddress}.
        </p>
    </div>
</div>
[/pre]
An important distinction to make with this approach is that the root node in the template is the container for the widget, and grabbing the innerHTML omits that element. Since I wanted a div wrapping the HTML, a second wrapping div had to be added.

Just like the first approach, a widget must be defined for the template binding to work. For this example I added several properties to the widget instead of a single object for my variables in the template.
[pre class="brush:javascript"]
    var MarkupBlock = declare([_Widget, _TemplatedMixin], {
        templateString: _markupBlockTemplate,
        // data for template
        fullName: "",
        occupation: "",
        emailAddress: ""
    });
[/pre]
Since I am not using the DTL for this example, my widget references the standard Dojo templated mixin. If you need to manipulate the markup based on the properties, you could use the DTL mixin instead, and the template structure from the first approach.

The widget is then used the same way as the first approach, defining a new instance with the values specified. This time, however, it doesn't get attached with the "placeAt" method. Instead, since the markup of the widget is needed, the "domNode.innerHTML" property is used to retrieve the markup. To demonstrate this, I created a textarea as a dojo attach point named "markupArea" in my parent widget, and set the value to the markup from the MarkupBlock.
[pre class="brush:javascript"]
    var sampleMarkup = new MarkupBlock({
        fullName: "Daved A",
        occupation: "Developer",
        emailAddress: "daved@domain.net"
    });
    this.markupArea.value = sampleMarkup.domNode.innerHTML;
[/pre]
Notice how there is only one wrapping div

I used this same approach to set the column formatter for my modified DGrid, and will touch on that in a future article.

Conclusion

My goal is to make widgets as easy as possible to manage in the future, and the modularity of Dojo - like it or not - makes things like this possible. Both of these techniques provide fairly simple and straight forward approaches to leveraging the Dojo template engines to keep markup out of your Dojo scripts. This allows for easier, more modularized maintenance of any HTML used for widget functions. It also makes it quicker and easier to swap out markup when needed, as you can edit the template HTML file directly, and leave the rest of your widget and scripts alone.

Articles for creating HTML or elements in Dojo

https://dojotoolkit.org/reference-guide/1.7/dojo/create.html
https://dojotoolkit.org/reference-guide/1.10/dojo/dom-construct.html
https://dojotoolkit.org/reference-guide/1.10/dojo/html.html
https://stackoverflow.com/questions/27403086/how-to-add-html-to-body-in-dojo

I found this SO question while wrapping this article up, and the last answer actually uses a similar approach to what I have here for the first approach.
https://stackoverflow.com/questions/27313412/mixin-more-than-one-template-in-a-dojo-widget

No comments:

Post a Comment