30 March 2016

© Copyright 2015-2019 by TECHNIA AB

All rights reserved.

PROPRIETARY RIGHTS NOTICE: This documentation is proprietary property of TECHNIA AB. In accordance with the terms and conditions of the Software License Agreement between the Customer and TECHNIA AB, the Customer is allowed to print as many copies as necessary of documentation copyrighted by TECHNIA relating to the software being used. This documentation shall be treated as confidential information and should be used only by employees or contractors with the Customer in accordance with the Agreement.

This product includes software developed by the Apache Software Foundation. (http://www.apache.org/).

2. Widget Tutorial

In this tutorial we will build a small client side widget that keeps track of the last edited and created objects. Things that will be covered:

  • Creating a new widget

  • Placing it on a dashboard

  • Writing javascript code and interacting with the following Helium javascript apis.

    • App.templates

    • App.ObjectStore

    • App.i18n

  • Creating and rendering a Handlebars template

2.1. Prerequisites

  • An running Helium environment

  • Basic javascript and HTML understanding

2.2. Creating the widget

The first thing to do is create a Widget definition. This is easily done be creating an xml file with the following content.

<?xml version="1.0" encoding="UTF-8"?>
<Widget xmlns="http://technia.com/helium/Widget">
    <Title>Latest changes</Title>
    <OnInit>App.custom.createLatestChanges</OnInit>
</Widget>

Add the file to your other widget definitions and give it a descriptive name, for instance LatestChanges.xml. The Title element is self explanatory, i.e it is the name of the widget and what will be rendered as an header. The OnInit element tells what javascript should be executed when the Helium framework instantiates the widget. It is also possible to pass additional parameters to the javascript function via the <OnOnitOption> element.

At the moment App.custom.createLatestChanges doesn’t exist but we will soon create it, but first will need to place our widget on a dashboard.

2.3. Placing the widget on a dashboard

Find a dashboard configuration and add the following to the list of widgets.

<Widgets>
    <!-- other widget definitions -->
    <Widget ref=":helium/LatestChanges.xml">
        <Id>latest-changes</Id>
        <Width>4</Width>
        <Height>4</Height>
        <X>9</X>
        <Y>0</Y>
        <Locked>false</Locked>
    </Widget>
</Widgets>

Here we reference our widget specific config via ref=":helium/LatestChanges.xml the other elements tells the widget where (<X> and <Y>) on the dashboard the widget should be rendered and what size it should be (<Width> and <Height>). With the widget placed on the dashboard, switch to your browser and reload the page containing your newly created widget.

You will be faced with an error message in the upper right corner and if you open up your development console in the browser you will find the following:

Unable to execute function 'App.custom.createLatestChanges(args)'. Does it exist? Error: TypeError: Cannot read property 'apply' of undefined

This means that the App.custom.createLatestChanges function doesn’t exist, which makes perfect sense, since we haven’t created it yet. Lets do that!

2.4. Creating the javascript

Create a javascript file in the custom folder and add the following to it:

(function(window, App, $) {
    "use strict";

    function LatestChanges(input) {
    }

    LatestChanges.prototype = {
    };

    var create = function(input) {
        return new LatestChanges(input);
    };

    App.custom = App.custom || {};
    App.custom.createLatestChanges = create;
})(window, window.App || {}, jQuery);

Most of this is boilerplate, but notice that we’re creating a function named create and later on we’re saving it to App.custom.createLatestChanges, the same function name Helium complained was missing earlier. Save the file and make sure it is deployed to your app-server. Reload the page and the error should be gone.

2.4.1. Creating a template

To render the content of the widget we’ll use a Handlebars template, that is, a snippet of html with some special markup for iterating and displaying variables.

Lets create the template with some mock data and place it the custom content folder. I gave it the name latest-changes.handlebars.

<div>
    <h1>Your latest changes</h1>
    <ul>
        <li><a href="#">Change 1</a></li>
        <li><a href="#">Change 2</a></li>
    </ul>
</div>

Then in our javascript we need to get a hold of the template and render it.

function LatestChanges(input) {
     this.$container = input.element.find('.widget-inner');
     this.template = App.templates.getTemplate(this.defaults.template);
     this._render();
}

LatestChanges.prototype = {
    defaults: {
        template: 'helium/custom/latest-changes',
    },

    _render: function() {
        this.$container.
            empty().
            append(this.template());
    }
};

Above we have zoomed in on the LatestChanges object and added some properties and an _render() helper method. The call to the App.templates.getTemplate(this.defaults.template) is our first interaction with the Helium api. It will fetch the template and compile it for us if it isn’t already compiled.

The _render method first clears our container element (this.$container) that we got from the element passed in to the object by the Helium framework via the constructor arguments (input). Then it appends our content by executing our template, this.template().

Save the file and reload your browser and you should see the template rendered in your widget. Now is a good time to add css and styling to your content but that is out of the scope for this tutorial. Lets instead add some dynamic functionality by listening to object changes.

2.4.2. Listening to object changes

To be able to listen for when an object is either changed or created we create two methods _onChange and _onCreate. These two methods are then added as listeners to App.ObjectStore.addChangeListener and App.ObjectStore.addCreateListener.

function LatestChanges(input) {
     this.$container = input.element.find('.widget-inner');
     this.template = App.templates.getTemplate(this.defaults.template);
     // listening on change and create events
     App.ObjectStore.addChangeListener(this._onChange.bind(this));
     App.ObjectStore.addCreateListener(this._onCreate.bind(this));
     this._render();
}

LatestChanges.prototype = {
    defaults: {
        template: 'helium/custom/latest-changes',
    },

    _render: function() {
        this.$container.
            empty().
            append(this.template());
    }
    // added onChange and onCreate methods
    _onChange: function(changedObjects) {
        App.log.debug('changed objects', changedObjects);
    },

    _onCreate: function(createdObjects) {
        App.log.debug('created objects', createdObjects);
    },
};

Now whenever an object is changed or created the _onChange and _onCreate methods will be called. Try it by editing a object and watch the edited object being printed in the developer console.

Next we will add the changed objects to our template and save them in the localStorage of the client.

2.5. Making the template dynamic

Until now the template we created have been hardcoded with mock data. Lets change that! First we need to create two methods that our widget will use to interact with the localstorage.

LatestChanges.prototype = {
    defaults: {
        template: 'helium/custom/latest-changes',
        localStorageKey: 'last-changed-objects'
    },

    _render: function() {
        this.$container.
            empty().
            append(this.template({objects: this._getLatestChangedObjects()}));
    }
    _onChange: function(changedObjects) {
        this._saveObjects(changedObjects, 'changed');
    },

    _onCreate: function(createdObjects) {
        this._saveObjects(createdObjects, 'created');
    },

    // Added this method that fetches objects from localStorage
    _getLatestChangedObjects: function() {
        var objects = window.localStorage.getItem(this.defaults.localStorageKey);
        if (objects) {
            return JSON.parse(objects);
        } else {
            return [];
        }
    },

    // Added this method that saves objects to localStorage
    _saveObjects: function(objects, type) {
        var currentObjects = this._getLatestChangedObjects();
        var newObjects = objects.map(function(object){
            var id = Object.keys(object)[0];
            var date = new Date();
            return {
                id: id,
                timestamp: date.toLocaleString(),
                name: object[id].data.name.values.map(function (value) {
                    return value.value;
                }).join(","),
                type: type
            }
        });
        newObjects = newObjects.concat(currentObjects).slice(0, this.defaults.maxNumberOfItems);
        window.localStorage.setItem(this.defaults.localStorageKey, JSON.stringify(newObjects));
        this._render();
    }
};

The two added methods _getLatestChangedObjects and _saveObjects respectively fetches objects from localStorage and saves them to localStorage when an change or create event is triggered.

One more important change to note is that when we’re rendering the template we’re passing in the objects from the localStorage via the call to _getLatestChangedObjects().

This will the make the list of changed objects available in our template. By changing the template to the following we can iterate over the objects. Inside the loop all properties we created when we saved the object is available, i.e. id, name, type and timestamp.

<div>
    <ul>
        {{#each objects}}
            <li><a href="javascript:App.routing.open('{{id}}');">{{name}}</a> was {{type}} at {{timestamp}}</li>
        {{else}}
            <li>You have no changes</li>
        {{/each}}
    </ul>
</div>

Note that if there is no changed or created objects present the message 'You have no changes' will be printed. Checkout Handlebars for more template information.

If you save your template and reload the page you should now have a widget displaying the most recently edited or created objects.

2.6. Internationalisation

Right now the widget works but it has no support for multiple languages since the strings in the template are hardcoded.

Let’s fix that! In your custom folder create an folder named lang and in that folder create a json file named after your current locale. My locale is set to en_us which means my file will be named en_us.json. Helium will try to find a translation file based on the locale of the browser, if no suitable file is found a default one will be used.

Add the following to the newly created file and make sure the file gets deployed to your server:

{
  "custom": {
    "latestChanges": {
      "change": "<a href='#' onclick='App.routing.open(\"{id}\")'>{name}</a> was {type} on {timestamp}",
      "noChanges": "You have not made any changes"
    }
  }
}

Here we’re creating an language file with keys that points to translations of different strings. For example to get a hold of the noChanges messages you would execute App.i18n.t('noChanges'). The change key points to a string with contains placeholders, to create a message based on that you would pass an additional object to the App.i18n.t function, i.e. App.i18n.t('changes' {id: 'id', name: 'name', type: 'type', timestamp: 'timestamp'});

Last, to make sure the widget works as expected, we need to make a couple of changes to our javascript and template.

First we need to pass the translation keys to our template.

_render: function() {
    this.$container.
        empty().
        append(this.template({
            changeKey: 'custom.latestChanges.change',
            changedObjects: this._getLatestChangedObjects(),
            noChangeKey: 'custom.latestChanges.noChanges'
        }));
},

Then we have to update our template to utilize the translation keys passed in. For that we’re going to use the i18n handlebars helper, which under the covers calls the App.i18n.t function.

<div>
    <ul>
        {{#each changedObjects}}
            <li>{{{i18n ../changeKey this}}}</li>
        {{{else}}
            <li>{{i18n ../noChangeKey}}</li>
        {{{/each}}
    </ul>
</div>

Inside of the {{#each loop we’re using the handlebars special syntax ../ to reach the variable outside of the loop. Since the i18n helper will return a string containing html that we don’t want to escape we’re using the {{{ handlebars syntax.

Now the widget is fully multilingual. If there is a need change the messages or add a new language you only need to create or edit the language files.

If you’ve been following along closely you might have noticed that there is an bug in our implementation. Since we’re separating objects based on whether they where created or changed (the type parameter sent to _saveObjects) and later using that type as a variable in our template.

This means that the words created and changed will show up in messages for all languages. There is lots of way to fix this for instance you might want to show an icon based on the type instead of showing the actual words. This is left as an exercise for the reader.

2.7. Final code

Here is the final code.

latest-changes-widget.js:

(function(window, App) {
    "use strict";

    function LatestChanges(input) {
        this.$container = input.element.find('.widget-inner');
        this.widget = input.widget;
        this.template = App.templates.getTemplate(this.defaults.template);
        this.widget.loader.stop();
        App.ObjectStore.addChangeListener(this._onChange.bind(this));
        App.ObjectStore.addCreateListener(this._onCreate.bind(this));
        this._render();
    }

    LatestChanges.prototype = {
        defaults: {
            template: 'helium/custom/latest-changes',
            maxNumberOfItems: 5,
            localStorageKey: 'last-changed-objects'
        },

        _render: function() {
            this.$container.
                empty().
                append(this.template({
                    changeKey: 'custom.latestChanges.change',
                    changedObjects: this._getLatestChangedObjects(),
                    noChangeKey: 'custom.latestChanges.noChanges'
                }));
        },

        _onChange: function(changedObjects) {
            this._saveObjects(changedObjects, 'changed');
        },

        _onCreate: function(createdObjects) {
            this._saveObjects(createdObjects, 'created');
        },

        _getLatestChangedObjects: function() {
            var objects = window.localStorage.getItem(this.defaults.localStorageKey);
            if (objects) {
                return JSON.parse(objects);
            } else {
                return [];
            }
        },

        _saveObjects: function(objects, type) {
            var currentObjects = this._getLatestChangedObjects();
            var newObjects = objects.map(function(object){
                var id = Object.keys(object)[0];
                var date = new Date();
                return {
                    id: id,
                    timestamp: date.toLocaleString(),
                    name: object[id].data.name.values.map(function (value) {
                        return value.value;
                    }).join(","),
                    type: type
                }
            });
            newObjects = newObjects.concat(currentObjects).slice(0, this.defaults.maxNumberOfItems);
            window.localStorage.setItem(this.defaults.localStorageKey, JSON.stringify(newObjects));
            this._render();
        }

    };

    var create = function(input) {
        return new LatestChanges(input);
    };

    App.custom = App.custom || {};
    App.custom.createLatestChanges = create;

})(window, window.App || {});

latest-changes.handlebars:

<div>
    <ul>
        {{#each changedObjects}}
            <li>{{{i18n ../changeKey this}}}</li>
        {{else}}
            <li>{{i18n ../noChangeKey}}</li>
        {{/each}}
    </ul>
</div>

LatestChanges.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Widget xmlns="http://technia.com/helium/Widget">
    <Title>Latest changes</Title>
    <OnInit>App.custom.createLatestChanges</OnInit>
</Widget>