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