Dashboard widgets

📘

Actual for Targetprocess v3.6.0+

Updated on Jan 22, 2015

Basic concepts

A dashboard is an organized collection of widgets.

A widget is a widget template applied to specific widget settings and placed on a dashboard.

A widget template is a specification of any content which the developer wants to display on a dashboard.

A widget templates should have a unique ID which is used to identify it in a widget gallery and to reference it from the dashboard settings. Therefore, it's recommended to use long and descriptive names as template IDs.

Basic template contract

The most basic widget template is an object with a string id field and insert function.

var template = {
    id: 'sampleWidget', // unique template ID
    insert: function(placeholder) {
        var content = document.createTextNode('This is a sample text');
        placeholder.appendChild(content);
    }
};

When added to a dashboard, a widget from this template will render sample text in placeholder , which is a container DOM element created by the dashboard for this specific widget instance.

🚧

It's strongly recommended to only use placeholder to insert the widget content. Do not traverse up its DOM tree to change parent containers or subscribe to events.

Register your widget template

The widget gallery is a collection of all widgets available in Targetprocess. It is the primary tool for users to discover and add widgets to their dashboards.

Unlike machines, regular users are not so good at operating the unique template IDs. To make their lives easier, you should add user-friendly information to your widget template to make it easier to find in a widget gallery.

var template = {
    id: 'company_rss_feed',

    // name and description are rendered on a widget card in a gallery
    name: 'Our company news',
    description: 'Displays the most recent entries from the RSS feed of our corporate blog',
    // user can use tags to quickly narrow down the widget list to a specific category
    tags: ['News', 'Company'],
    // image rendered on a widget card helps the user to visually identify the template in a list
    previewSrc: '../img/widget-rss-feed.png',

    //insert: function() ...
};

To let the application know about your new widget template and to make it available in a widget gallery for the end users, you need to register it in a widget template registry.

configurator.getDashboardWidgetTemplateRegistry().addWidgetTemplate(template);

You don't have to fire any events when adding widgets asynchronously. The registry handles all changes and automatically notifies the gallery and dashboards about new templates.

Widget settings

Most widgets may need to store some settings to let the user configure widget behavior. Such settings can be stored by the dashboard in a uniform way, so there is no need for a developer to invent his own storage. Settings are stored as a single object which is passed as the 2nd argument to the template's insert function.

var template = {
    id: 'sampleGreetingWidget',
    insert: function(placeholder, settings) {
        var content = document.createTextNode('Hello, ' + settings.name + '!');
        placeholder.appendChild(content);
    }
};

You can also specify the default settings value to use when users add a widget to a dashboard.

var template = {
    id: 'sampleGreetingWidget',
    defaultSettings: {
        name: 'John'
    },
    insert: ...
};

Settings view

Of course you would want to let users change widget settings. Fortunately, the dashboard service takes care of rendering a consistent settings dialogs for you, so you only need to tell it how to render settings editor content in a parent container. To do that, add an insertSettings function to the template definition.

var template = {
    // ...
    insertSettings: function(placeholder, currentSettings) {
        // create a text input to let the user change stored name value
        var nameEditor = document.createElement('input');
        nameEditor.value = currentSettings.name;
        placeholder.appendChild(nameEditor);

        // will be called when user decides to apply settings changes
        return function() {
            return {
                name: nameEditor.value
            };
        };
    }
};

The insertSettings function is quite similar to the template's insert function. It receives a placeholder, a DOM element which you can fill with settings editors of your choice, and currentSettings, an object representing the currently stored settings of this specific widget instance.

To let a user apply new settings, return a function from insertSettings which returns an object with new settings when called. Typically, this function will read state attributes from the inputs of the editor view you've created inside insertSettings.

🚧

Due to the way settings dialog is rendered it is highly recommended to avoid resizing of the rendered settings element. If your settings editor has a dynamically displayed content, reserve the screen space for it in advance and render it in a container with fixed width and height. Modifying the size of the rendered settings content will likely cause negative visual effects.

In-place widget updates

By default, when a user clicks the confirmation button in the widget settings dialog, the existing widget is destroyed and a new one is created on a dashboard with the new settings. This may be undesirable in some cases, e.g. when the widget is "heavy" and loads a lot of data upon initialization. It would be natural to support in-place updates to let you apply new settings to the existing widget instance.

To do that, change the template's insert function to return an object with an update function, which will receive new settings value when user applies them.

var template = {
    id: 'sampleWidget',
    insert: function(placeholder, settings) {
        // var view = new ContentView(placeholder, settings);

        return {
            update: function(newSettings) {
                // view.setSettings(newSettings);
            }
        };
    }
};

Integration

You can modify the template's insert function to control the widget's lifecycle and integrate with the outside world.

Clean up widget resources

To clean up widget resources (e.g. to unsubscribe from global events when the widget is removed from a dashboard), modify the template's insert function to return an object with the destroy function.

var template = {
    id: 'sampleWidget',
    insert: function(placeholder) {
        // var view = new Widget(placeholder);
        return {
            destroy: function() {
                // view.cleanUp();
            }
        };
    }
};

The destroy callback is called when the dashboard decides to remove the DOM element of the widget instance from a document.

Use application context

If your widget works with Targetprocess entities, it is likely that it needs to filter them according to the Team/Project context that's specified for the dashboard.

To get information about the current Project/Team selection, use the environment argument passed to the template's insert function. If you would like to be notified whenever a user switches the selection, add an updateContext callback to the return value of the insert function. Use the passed new context value to update your widget's data accordingly.

var template = {
    id: 'sampleContextWidget',
    insert: function(placeholder, settings, environment) {
        // var view = new ContextView(environment.context.acid);

        return {
            updateContext: function(newContext) {
                // view.setNewContext(newContext.acid);
            }
        };
    }
};

Handle resize events

Your widget may depend on the screen size of its container. For example, when you render a data chart and need to adjust it visually in a container element when a browser window is resized or when a dashboard layout is changed. To get notified about such events, add a resize function to the return value of the template's insert function.

var template = {
    id: 'sampleResizingWidget',
    insert: function() {
        // var view = new Widget();
        return {
            resize: function() {
                // view.handleResize();
            }
        };
    }
};

Writing asynchronous widgets

Most of the time, your widget will need to load some data before it can display anything meaningful. This may take some time when calling asynchronous APIs. To prevent the user from staring at blank containers and to keep the developer from writing inconsistent and boilerplate loaders, the dashboards naturally support asynchronous widget rendering.

To render the widget asynchronously, return a promise from the template's insert function. The promise should be resolved when all async operations required by the widget are completed and the widget itself is inserted into a placeholder. Integration callbacks like update, destroy, etc. should be placed into the future value produced by the promise.

var template = {
    id: 'sampleAsyncWidget',
    insert: function(placeholder) {
        return loadData()
            .then(function(data) {
                var view = new Widget(placeholder, data);

                return {
                    update: ..., // handle widget updates if required
                    destroy: view.destroy.bind(view)
                };
            });
    }
};

The same applies to rendering widget settings dialog. If you need to load some data before displaying the settings, return a promise from insertSettings, and resolve it when the async operations are completed and the settings editor is fully rendered. The promise value should contain a function to apply selected settings.

var template = {
    ...
    insertSettings: function(placeholder, settings) {
        return loadData()
            .then(function(data) {
                var settingsView = new SettingsView(placeholder, data, settings);
                return function() {
                    return settingsView.getSettingsState();
                };
            });
    }
};

🚧

Due to the way that settings dialog is rendered, it is highly recommended to resolve the promise when the settings view has all the data it needs when it is rendered in its final state. Modifying the size of the rendered settings content will likely cause negative visual effects.

Creating widgets with React

We find it quite convenient to build widgets using the React library.

Usually you would define React classes for the widget itself and for the widget settings editor, and then render them with the current settings in insert and insertSettings respectively.

Here is a complete example of a template that greets users with the name stored in widget settings.

define(function(require) {
    var React = require('react');
    var SettingsListItem = require('jsx!tau/components/dashboard/widget.templates/shared/settings.list.item.view');
    var SettingsContainer = require('jsx!tau/components/dashboard/widget.templates/shared/settings.container.view');

    // Widget class should be defined only once per template.
    // Define it here instead of template's `insert`.
    var Widget = React.createClass({
        render() {
            return <div>Hello, {this.props.name}!</div>;
        }
    });

    // Same here - define a class only once.
    var WidgetSettings = React.createClass({
        mixins: [React.addons.LinkedStateMixin],

        getInitialState() {
            return { name: this.props.initialName };
        },

        render() {
            // Use helper "container" and "settings item" components.
            // State link is only used for simplicity here.
            return (
                <SettingsContainer>
                    <SettingsListItem title="Name">
                        <input type="text" valueLink={this.linkState("name")}/>
                    </SettingsListItem>
                </SettingsContainer>
            );
        }
    });

    // Export a construction function which accepts a configurator
    // to get all required application services.
    return function(configurator) {
        return {
            id: 'sampleReactWidget',
            name: 'Sample widget built with React',
            description: 'Demonstrates how to use React to create widget templates',
            tags: ['Sample'],

            defaultSettings: { name: 'User' },

            insert: function(placeholder, settings) {
                // Pass current settings as component's props.
                var view = React.render(React.createElement(Widget, settings), placeholder);
                // Update a component in-place by wiring up `update` callback to component's `setProps`.
                return { update: view.setProps.bind(view) };
            },

            insertSettings: function(placeholder, settings) {
                // Create initialization props for settings editor from current settings.
                var props = { initialName: settings.name };
                var view = React.render(React.createElement(WidgetSettings, props), placeholder);

                // Just return a component state when applying widget settings.
                // You may want to pick the required props only instead of returning entire state here.
                return function() {
                    return view.state;
                };
            }
        };
    };
});