Méthode Swing dashboards are extensible with custom widgets.

This document will introduce the use of Méthode Swing SDK for creating and testing the widgets, as well as describe the correct widget structure and provide a complete example of a custom widget creation.

Widget structure

All the widgets are put inside the following path:

{SWING-APP}/app/widgets/{WIDGET-NAME}

Widgets can be configured outside {SWING-APP}. See the paragraph How to configure widgets in external folder for some tips in how to take advantage of this new configuration option.

As you can see, all the widget provided by Méthode Swing are included in this folder.

Each widget folder must contain the following files:

Table 1. Files in {WIDGET-NAME} folder
File name Description

{WIDGET-NAME} .js

The JavaScript file defines the widget’s behaviour.

{WIDGET-NAME} .html

The file represents the visual template of the widget.

{WIDGET-NAME} .less

LESS is a CSS-like language. This file changes the aesthetic appearance of the widget. It is not mandatory.

It is necessary that the file all have the same name of the folder. It is recommended, although not mandatory, to use lower case characters and, in general, to avoid "unusual" characters.

DEBUG MODE: by default, all widgets are aggregated into a single widgets.js file at the Tomcat startup. When creating a widget, it may be frustrating to restart the Tomcat Server every time a change is made.

To avoid this, set the configuration property debugEnabled to true.

Then, that widget will be loaded on every refresh of the page.

Note: in debug mode, the widgets are loaded separately and asynchronously; this may occur in a widget not loaded correctly. In this case, simply change tab (e.g. move to "My Area") and back to the Dashboard to have the widget loaded correctly.

JS file

The widget’s Javascript file should be written as follows:

/*
 * Todo List Widget. Used to demonstrate the creation of a custom widget
 */
eidosmedia.webclient.dashboard.widgets.define( {WIDGET-NAME}, function() {

    var settings = {
        title: 'SOME TITLE',
        icon: 'icon-gear'
        // Default settings...
    };

    function onResize() {
        // Method called on the windows resize.
    };

    function loadData() {
        // Method called on the widget loading. Used to retrieve the user's data.
    };

    function updateData() {
        // Used to update the widget's template.
    };

    return {
        settings: settings,
        onResize: onResize,
        loadData: loadData,
        updateData: updateData
    };

});

Basically, is the widget should call the

eidosmedia.webclient.dashboard.widgets.define

function, with the following parameters:

  • {WIDGET-NAME} : the same name used everywhere else in the widget folder.

  • a function which returns a JavaScript object with a set of mandatory methods as described in the following paragraph.

Each widget has a set of default settings, which can be specified under the settings property of the widget.

Before moving to the various methods to be specified for the widget, it is necessary to describe the "Event Chain" that happens when a widget is loaded. The chain of events is described in the following picture:

Widget Chain of events
Figure 1. Widget Chain of events

When the Dashboard is displayed, the widgets specified in the configuration are initialized. The chain of events continues as follows:

  1. First, the dashboard renders the HTML template with status = "init". ( See HTML file for details on how the template is called )

  2. Second, the dashboard calls the widget’s loadData (required) method which should load the widget’s data. ( See loadData for details on how the method is called )

  3. Within the widget loadData method, the widget should call either the dashboard updateBaseData method ( See updateBaseData for further details ), to let the dashboard know that the loading has finished.

Optionally, it is possible to specify the behaviour when the window is resized. (For example, it can be used to retrive the number of visible objects and change the widget’s rendering accordingly).

The figure below shows the Resize event.

Widget Chain of events
Figure 2. Widget Resize

See onResize for further details on how to manage the resize.

Required methods for the widget

loadData

loadData is the method called by the Dashboard in order to load the Widget data. It must be declared as follows:

loadData: function( settings, ctx ) { ...

The function is called with one parameter:

  • settings - contains the settings specified in the widget configuration. It is a Javascript object.

  • ctx - the Swing context object. See Context object documentation.

If you specified the settings property in the widget declaration, you can be sure that at least those settings are available. If the user specifies customs settings in Swing configuration, the properties with the same name are overridden by the user configuration.

Within the loadData function, the Javascript context (also known as the this object) is the widgetContext object ( see widgetContext: list of available methods within the widget to know the complete list of methods available ).

It is mandatory that the widget calls the updateBaseData method of the widgetContext object (the this object), otherwise the dashboard as no way of knowing that the widget has finished loading. See updateBaseData for further details.

In general, the method updateBaseData must be called with two parameters:

  • settings: the settings described above.

  • data : the loaded data. It can be whatever you prefer (an array or JSON object). The data is passed to the widget without further elaboration.

This is an example of the TODOLIST widget loadData function.

function loadData( settings, ctx ) {
    var widgetContext = this; // save the widget variable.
    // Get the user todo list
    try {
        /* getUserData is one of the available methods,
           see the proper documentation below for further details */
        widgetContext.getUserData( 'todolist',{
            success: function( data ) {
                // Process List items.

                /* NOTE: IT IS NECESSARY TO CALL THE FOLLOWING METHOD TO LET THE DASHBOARD
                   KNOW THAT THE WIDGET HAS COMPLETED THE DATA LOADING. */
                widgetContext.updateBaseData( settings, data );
            },
            error: function(xhr, textStatus, errorThrown) {
                // Process the error response and show the Widget Error
                var error = widgetContext.processErrorResponse(xhr, textStatus, errorThrown);
                widgetContext.showError(error);
            }
        });
    } catch (ex) {
        widgetContext.showError( { message: ex.message } );
    }
};

Optional methods for the widget

updateData

updateData is the method called by the Dashboard after the widget has been loaded. It is generally used to add listeners for events, button clicks, and anything related.

Do not confuse updateData (the widget method) with updateBaseData (the widgetContext method).

The event is descrived in the following picture:

updateData and updateBaseData
Figure 3. updateData and updateBaseData

The widgetContext.updateBaseData() checks if the widget updateData method is available and calls it.

updateData: function( settings, items, ctx ) { ...

The function is called with some parameters:

  • settings - contains the settings specified in the widget configuration. It is a Javascript object.

  • data - the loaded data. It can be whatever you prefer (an array or JSON object). The data is passed to the widget without further elaboration.

  • ctx - the Swing context object. See Context object documentation.

Within the updateData function, the Javascript context (also known as the this object) is the widgetContext object ( see widgetContext: list of available methods within the widget to know the complete list of methods available ).

This is an example of the updateData function.

function updateData( settings, data, ctx ) {
    var widgetContext = this; // save the widget variable.

    // add Listeners to the object.

    var $widget = widgetContext.getWidgetContainer(); //Obtains the jQuery reference to the widget.

    $widget.find('#saveSomething').off('click').on('click', function() {
        // do some elaboration of the data...
        alert('you clicked me');
    });

};
onResize

onResize is the method called by the Dashboard whenever the Browser window is resized. It can be used, for example, to change the number of rows shown.

onResize: function( ) { ...

The method is called without parameters.

Within the onResize function, the Javascript context (also known as the this object) is the widgetContext object ( see widgetContext: list of available methods within the widget to know the complete list of methods available ).

This is an example of the diggfeeds widget onResize function.

function onResize() {
    var widgetContext = this;
    var ROW_PIXEL_HEIGHT = 65;
    var HEADER_PIXEL_HEIGHT = 30;

    var calcElements = function() {
        var widgetHeight = widgetContext.getHeight();
        var totalEl = Math.floor( (widgetHeight - HEADER_PIXEL_HEIGHT) / ROW_PIXEL_HEIGHT);
        return totalEl;
    };

    var totalRow = widgetContext.settings.count;
    var totalNewEl = calcElements.call(this);

    if ( totalNewEl === totalRow ){
        return;
    } else {
        // New quantity of items.
        // Refresh the widget with new items.
        this.settings.count = totalNewEl;
        loadData.call( widgetContext, widgetContext.settings );
    }
};

widgetContext: list of available methods within the widget

The widgetContext object is very important and represents the context ( the this object ) with which the loadData, updateData and onResize methods are called.

It provides a set of common methods which can ( and should ) be used within the widget.

The most important is updateBaseData, which must be called within the widget loadData method as widely described before.

Any of these methods can be accessed with the keyword this within the threed common methods ( loadData, updateData and onResize ).

It is recommended to save the reference into a variable, to be used within the callback functions. For example:

var widgetContext = this;
// ....
widgetContext.updateBaseData( settings, data );
updateBaseData

The method must be called within the loadData method, to let the dashboard know that the widget has finished loading data. It must be called as follows:

widgetContext.updateBaseData( settings, data );
  • settings are the widget’s settings ( provided within the loadData method ), or any setting that you want to pass to the widget.

  • data is the loaded information to be passed to the widget.

This internal method automatically calls the widget updateData method with the same settings, as described above.
getHeight

Returns the current widget height. Useful in obtaining the widget dimensions for the onResize method.

function onResize() {
    var widgetHeight = this.getHeight();
    // Some elaboration...
}
getWidth

Returns the current widget width. Useful in obtaining the widget dimensions for the onResize method.

function onResize() {
    var widgetWidth = this.getWidth();
    // Some elaboration...
}
getWidgetIcon

Returns the current widget icon as specified in the settings. It is often used inside the widget HTML template, as follows.

The result of this.getWidgetIcon() and of this.settings.icon are the same.
<h1>
    <%=settings.title%>
    <i class="<%=(widget.getWidgetIcon() || settings.icon)%>"></i>
</h1>
getIcon

getIcon returns the configured icon according to the type. See here for details on the configuration.

It is called as follows:

this.getIcon( type );

It returns a string containing the corresponding icon.

    var icon = this.getIcon( 'EOM::Story' ); // Returns 'emui-icon-file-text';
    // Some elaboration...
addDummy

addDummy fills an array with empty elements.

It is called as follows:

addDummy(data, max)
  • data is the array of items

  • max is the array length.

    // data has 30 elements.
    this.addDummy( data, 50 );
    /**
    *  Now data has 50 elements. From Index 30 to index 49, the element is
    * { empty: true }
    */
execute

execute executes specific supported actions.

Supported actions are:

  • newcontent : opens the "New Content" dialog

  • newmessage : opens the "New Message" dialog

  • newstory : creates a new story document with the default configuration

  • newgallery : creates a new gallery document with the default configuration

It is called as follows:

execute( action, options )

The method does not return anything.

    // Open the "new content" dialog.
    this.execute( "newcontent" );
getWidgetContainer

The method returns the jQuery reference to the current widget’s container

Use this method to quickly look for DOM elements of your widget.
    var $container = this.getWidgetContainer();

    // Search all anchors inside the widget...

    var $links = $container.find('a');
getCurrentUser

The method returns a JSON object with the logged user’s properties. See the example to see the returned properties.

    var currentUser = this.getCurrentUser();

    /** Returns

    {
        "picture": "/WebClient/user/avatar/user_name?token=1fb301ce-1dc3-4f78-9e2c-0b9c2750ff1b",
        "name": "user_name",
        "description": "Eidosmedia top class R&D",
        "config_folder": "eomfs:/Configurations/Profiles/user_name/Config",
        "id": "1.0.417997841",
        "fullName": "Ted Mosby",
        "team": "Globe_Web",
        "phoneNumber": "456",
        "mobileNumber": "123",
        "twitter": "user_name",
        "facebook": "john.doe@r.com",
        "role": "Reporter",
        "homeEmail": "john.doe.home@r.com",
        "businessEmail": "john.doe@r.com2",
        "statusMessage": "Status msgsdsadsadas",
        "workDir": "workfolder:///Globe/Sport",
        "location": "Roma",
        "profileDir": "eomfs:/Configurations/Profiles/user_name",
        "lastLoggedOn": 1423501287,
        "initials": "AP",
        "signature": "John",
        "system_attributes": "SYSTEM ATTRIBUTES",
        "metadata": "",
        "homePath": "/Users/user_name",
        "admin": true,
        "groups": [
            {
                "name": "Administrators",
                "description": "",
                "config_folder": "eomfs:/Configurations/Profiles/Administrators/Config",
                "profile_folder": "eomfs:/Configurations/Profiles/Administrators",
                "categories": [
                    "EOM::Group",
                    "EOM::Configuration",
                    "EOM::Privileges"
                ],
                "isConfiguration": true
            }
        ],
        "calendars": [
            {
                "color": "#0C96D7",
                "id": "U00033812843qQO",
                "name": "World Cup 2014",
                "url": "https://www.google.com/calendar/ical/qlbi1b0rp50vb07rmssnebq694%40group.calendar.google.com/public/basic.ics",
                "icon": "icon-bookmark",
                "private": false
            }
        ],
        "status": "available",
        "teams": [
            "Globe_Team",
            "Globe_Web"
        ]
    }

    **/
getUserLocation

Returns the Backbone Model of the user location.

    var userLocation = this.getUserLocation();
    var latitude = userLocation.get('latitude'); // longitude...
getCurrentLocation

Returns, in a callback, the current location obtained with the browser geolocation API and with Google Maps API.

    this.getCurrentLocation( function(location) {

        // Do something with location...
        // It is an object
        /*
        {
            "latitude": 45.3231,
            "longitude": 9.1321
        }
        */

    });
getConfiguration

Returns an object with specific parts of the configuration. It is called as follows:

 var config = this.getConfiguration();

The result is a JSON object with the following properties:

  • config.isCacheEnabled() : returns true if the cache is enabled.

  • config.language : the current user language

  • config.location : the "location" object configured in the config.json file.

getPersistence

Returns the persistence services that enables to perform cached AJAX call.

 var persistence = this.getPersistence();

An example from the Digg feeds widget:

// Executes an AJAX call to obtain the DIGG feeds.
    persistence.ajax({
        emCache: {
            disabled: !this.getConfiguration().isCacheEnabled(),
            prefix: 'eidosmedia.widget|diggfeeds|',
            ttl: 600,
            safe: true
        },
        url : widgetContext.buildUrl( '/ws/widgets/digg/mostDuggFeed', widgetContext.getAppContext() ),
        data : request,
        success : function(respData) {
            if ( count < respData.length ) {
                respData = respData.splice(0, count);
            }
            response = widgetContext.addDummy(respData, count);

            // Update the widget data with the standard method, as we don't override it.
            widgetContext.updateBaseData( widgetContext.settings, response);
            // Add custom listeners.
            addListeners.call( widgetContext, response );
        },
        error : function(xhr, textStatus, errorThrown) {
            var error = widgetContext
                            .processErrorResponse(xhr, textStatus, errorThrown);
            widgetContext.showError(error);
        }
    });
processErrorResponse, showError

The processErrorResponse method is called to create a useful Javascript object, to be used (often) with the showError method.

The showError method is used to display an error message inside the widget container.

processErrorResponse is called as follows:

var errorObj = this.processErrorResponse( xhr, textStatus, errorThrown );

showError is called as follows:

this.showError( errorObj );

The correct usage for processErrorResponse is inside a jQuery AJAX error callback, as seen in the example:

var widgetContext = this;
// Executes an AJAX call to obtain the DIGG feeds.
$.ajax({
    url: '',
    data: [],
    success : function(respData) {
        // ...
    },
    error : function(xhr, textStatus, errorThrown) {
        var errorObj = widgetContext
                        .processErrorResponse(xhr, textStatus, errorThrown);
        widgetContext.showError( errorObj );
    }
});

showError can be called also without using processErrorResponse, as the example shows:

    this.showError( { message: 'Some error details here' } );
Widget Error
Figure 4. Widget Error
translate

translate is exactly like using $.eomtranslate as seen in the Localization documentation ( here ).

Example:

    var translatedString = this.translate( 'favourites.addyourselferror', 'This is the default message for Adding yourself to Favourites is not allowed.');

    // or, if data is needed...

    var msg = $.eomtranslate('newmessagemodal.sendsuccess', {
          data: ['elem1', 'elem2', 'elem3', 'elem4' ],
          defaultMessage: 'The message was successfully sent to ' + users
     });
translateWidget

translateWidget translates all the HTML inside the widget container. It uses data-i18n attributes to perform the translation. See here for details.

this.translateWidget();
getUserData, addUserData, removeUserData, setUserData

This methods allow to get, add, remove, and set specific data inside the user’s profile folder.

Syntax is as follows:

    this.getUserData( name, callbackSettings ); // Obtains the user data.

    this.addUserData( data, name, callbackSettings ); // Add some data to the user's data.

    this.removeUserData( data, name, callbackSettings ); // Remove some data from the user's data.

    this.setUserData( data, name, callbackSettings ); // Set the user data to some value.

In general, the parameters are:

  • data: the data to add, remove, set from the file.

  • name: the name of the file to be stored in the User profile folder, without the extension (e.g. 'recents', 'todolist', …​; it creates a 'recents.json' file into the User profile folder)

  • callbackSettings: a Javascript object with two parameters (success and error, which are both functions), to call the methods asynchronously. If omitted, the call is synchronous.

Examples:

    // assuming that, before, we had a 'var widgetContext = this;'...

    // GET USER DATA

    // Get recents asynchronously (recommended).
    this.getUserData('recents', {
        success: function( recents ) {
            //...
        },
        error: function(xhr, textStatus, errorThrown) {
            var errorObj = widgetContext
                        .processErrorResponse(xhr, textStatus, errorThrown);
            widgetContext.showError( errorObj );
        }
    });

    var recents = this.getUserData('recents'); // Get recents synchronously (not recommended)

    // ADD USER DATA (if the file does not exist, it is created).

    // add a recent to the list (sync).
    this.addUserData( { /* item */ }, 'recents', {
        success: function( newRecents ) {
            // returns the update list of recents
            //...
        },
        error: function(xhr, textStatus, errorThrown) {
            var errorObj = widgetContext
                        .processErrorResponse(xhr, textStatus, errorThrown);
            widgetContext.showError( errorObj );
        }
    });

    // add a recent to the list (async).
    var newRecents = this.addUserData( { /* item */ }, 'recents' );

    // REMOVE USER DATA

    // remove a recent from the list (sync).
    this.removeUserData( { /* item identifier */ }, 'recents', {
        success: function( newRecents ) {
            // returns the update list of recents
            //...
        },
        error: function(xhr, textStatus, errorThrown) {
            var errorObj = widgetContext
                        .processErrorResponse(xhr, textStatus, errorThrown);
            widgetContext.showError( errorObj );
        }
    });

    // add a recent to the list (async).
    var newRecents = this.removeUserData( { /* item */ }, 'recents' );

    // SET USER DATA (overrides the current content with the new one).

    // completely sets the new todo list (sync)
    this.setUserData( [ {}, {}, }{} ], 'todolist', {
        success: function( newTodoList ) {
            // returns the update Todo List
            //...
        },
        error: function(xhr, textStatus, errorThrown) {
            var errorObj = widgetContext
                        .processErrorResponse(xhr, textStatus, errorThrown);
            widgetContext.showError( errorObj );
        }
    });

    // completely sets the new todo list (async).
    var newTodoList = this.setUserData( [ {}, {}, }{} ], 'todolist' );
getPreview

Returns the URL of the server-side generated preview. Useful for Méthode Objects.

var preview = this.getPreview( id ); // e.g. this.getPreview( '0$1.231321421' );
// It returns a valid URL
openPreview

Opens the M&ecute;thode Swing Preview dialog.

this.openPreview( items, options );

The method accepts 2 parameters:

  • items: (object or array), mandatory - a Javascript Object representing the item information returd by query or a folder nativation/ Array including info on the file to load

  • options: (object), not mandatory - a Javascript Object including the preview options.

The options object is a Javascript object with the following properties:

  • loadObject (default is false) - Reloads the item information.

Example:

    this.openPreview( items, { loadObject: true });

HTML file

The HTML template can be as free as desired. The only contraint is that the whole HTML code should be wrapped into a div, such as the following example:

<div>
</div>

The widget can be have a fixed height, a minimum height or can be responsive. This enables the Swing dashboard to adapt according to the screen dimensions.

To have a fixed height widget, specify the height in the style attribute of the div, as follows:

<div style="height:300px;">
<!-- ... -->
</div>

To have a widget with minimum height, specify the min-height in the style attribute of the div, as follows:

<div style="min-height:300px;">
<!-- ... -->
</div>

To have a responsive widget (a widget that occupies the available height), add the class emui-widget-vfill to the div.

<div class="emui-widget-vfill">
<!-- ... -->
</div>

Méthode Swing uses Underscore.JS templating system to enhance the user experience.

See Underscore.JS for further information, or the box below for some useful tips.

Inside the template is then possible to use the Underscore.JS syntax, and use the methods and objects described in the following section.

Available information inside the HTML template

As described above, the HTML template is called with the Underscore.JS template syntax. Inside the HTML template, it is possible to use the following objects:

  • widget: it contains the same methods described in the widgetContext section above. See widgetContext: list of available methods within the widget section for further details on the available methods.

  • settings: it is a Javascript object containing the widget settings as specified in the configuration.

  • data: contains all the data available to the widget( the ones specified in the updateData and loadData method described above ).

  • status: either 'init' or 'loaded', for the first rendering ( before loadData ) and the last one ( after updateData ).

Sample HTML template

<%
    var title = data.title || "";
    title =  '<a href="#explore?query='+ data.id +'" class="emui-link">' + title +'</a>';
%>

<div class="emui-table-gallery emui-widget-vfill">
  <h1>
    <i class="<%=(settings.icon || 'icon-picture')%>"></i><%=title%></h1>
    <div class="list">
    <ul>
        <% _.each(data.items, function( item, index ) {
            var publishedDate = '',
                image = '';
            if (!item.empty) {
              publishedDate = item.publishedDate;
              image = ' style="cursor:pointer;background-image:url(' + widget.getPreview( item.id ) +'); background-size: cover;"  data-preview="image" data-preview-index="' + index + '"';
            }

            var preview = '<div class="image"' + image + '>';
            preview += '<div class="info">' + publishedDate +'</div>';
            preview +='</div>';
         %>
        <li><%=preview%></li>
    <% }); %>
  </ul>
  </div>
</div>

LESS file

Less is a CSS pre-processor, meaning that it extends the CSS language, adding features that allow variables, mixins, functions and many other techniques that allow you to make CSS that is more maintainable, themable and extendable.

LESS finally becomes CSS. Please refer to LESS website for further details.

Variable reference

Méthode Swing provides a set of common variables, as well as a set of predefined colors to maintain the coherence with Méthode Swing color palette.

To include the variable file inside your LESS, insert the following line at the beginning:

@import (reference) "../less/widgets-header.less";

Here follows an excerpt of the available variables:

// -------------------------
@black:                 #000;
@grayDarker:            #080808;
@grayDark:              #999;
@gray:                  #bcbcbc;    // view background
@grayLight:             #d8d8d8;    // body background
@grayLighter:           #e8e8e8;
@grayLighter2:          #f8f8f8;
@grayLightest:          #f8f8f8;
@white:                 #fff;

// Accent colors
// -------------------------
@blue:                  #a5c0dc;
@blueDark:              #4e84bb;
@blueLighter:           lighten(@blueDark, 10%);
@green:                 #77d89a;
@red:                   #9d261d;
@lightRed:              #E24242;
@darkRed:               darken(@lightRed, 10%);
@yellow:                #ffdb80;
@orange:                #ff9f6b;
@pink:                  #c3325f;
@purple:                #7a43b6;

Which can be used as follows:

.selector {
    background-color: @blueDark;
}

Again, please refer to LESS website for further details on how to write LESS files.

How to configure widgets in external folder

Additional widgets may be made available to Swing application by defining one or more nested components in Swing web context in server.xml. (for further details on Tomcat 9.x Resources configuration, please refer to Resources configuration).

This is an example of the external widgets configuration.

<Context docBase="com.eidosmedia.webclient.web-app"
    path="/swing" reloadable="false">
    <Resources
        className="org.apache.catalina.webresources.StandardRoot">
        <PreResources
            className="org.apache.catalina.webresources.DirResourceSet"
            base="/methode/meth01/extension/widgets" readOnly="true"
            webAppMount="/app/widgets" />
    </Resources>
</Context>