2015-08-14

CTools and RequireJS

This is huge. I mean - really huge. One of the biggest advances in Ctools in it's history. A complete refactor has been made to provide support for RequireJS. From that page page:

RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code

Ok, this sounds boring. And maybe is. But what this bring to the table is not. Including a dashboard in an external app is just as simple as loading the RequireJS (and the associated configurations) onto a page, nothing else. The rest of the resources you may need, will be loaded via RequireJS, this guarantees you only load what you use.

We guarantee that there are no more global objects - including the old Dashboards object that had all the logic. And this opens the possibility - you guess it - for very simply having multiple dashboards on a single page.

We're actually very confident already that we're very much on the right track; This will be the default when 6.0 comes out...

CDF

We collected the documentation in here., that I'm including in this blog post as well. You'll see this improvements, both on CDF and CDE, since CTools version 15.06.30, so it's even possible that you can start taking advantage of this already.

Quick start to it? Open the plugin samples and see the differences for yourself. This is not backward compatible, so you need to adapt the code to the new style We have the same samples for the same dashboards using the new and legacy mode



New require-js samples

New Dashboard Structure

The dashboard structure, itself, remains pretty much the same, the big difference here is: nothing is published to the global scope. From now on, there will be no Dashboards global object, no render_componentName objects polluting the global scope, vulnerable to being tampered with.

Do note that for debugging purposes, you can export your dashboard object to the window object, so it becomes available in the developer console.

Example:

require(['cdf/Dashboard.Blueprint', 'cdf/components/SelectComponent'], function(Dashboard, SelectComponent) {

var dashboard = new Dashboard();

dashboard.addParameter("region", "1");

dashboard.addComponent(new SelectComponent({

name: "selectComponent",

type: "selectComponent",

parameters: [],

valuesArray: [

["1", "Lisbon"],

["2", "Dusseldorf"]

],

parameter: "region",

valueAsId: false,

htmlObject: "sampleObject",

executeAtStart: true,

postChange: function() {

alert("You chose: " + this.dashboard.getParameterValue(this.parameter));

}

}));

dashboard.init();

// I'm still developing this dashboard, so I want this here to debug it.

// It will be removed when the dashboard is ready though.

window.dashboard = dashboard;

});

Reviewed API

The API had to be reviewed a bit, to accommodate for the paradigm change. It won't be hard to make the shift, though:

require(['cdf/Dashboard.Blueprint', 'cdf/Logger', ...], function(Dashboard, Logger, ...) {

var dashboard = new Dashboard();

//logic

dashboard.addParameter("startTime", new Date().getTime());

dashboard.init();

var dashboardInitTime = new Date().getTime() - dashboard.getParameterValue("startTime");

Loggerlog("Dashboard initiated in " + dashboardInitTime + "ms");

//...

});
So, before, we had a Dashboards global object which was used has a broker to the dashboard internals, you would want to access dashboard parameters and/or components either inside the dashboard itself, or out. You still can, the difference is that now you must use the reference to the dashboard you created, like dashboard in the example above. Inside the components code, you don't need to know the reference of the dashboard, each component, when added to a dashboard, will automatically receive a reference to it, accessible using this.dashboard. This dashboard reference, has most of the functions the global object Dashboards had, however, not all.

The log for example was moved outside, and you will have to require it if you want to use it:

require(["cdf/Logger"], function(Logger) {

Logger.log("log...");

});
Note: If you will use Logger a lot, consider requiring it with your dashboard definition:

require(['cdf/Dashboard.Blueprint', ..., 'cdf/Logger'], function(Dashboard, ..., Logger) {

...

});

Events that can be consumed externally

The events functionality is extended from Backbone.Events, so you can learn to use it to the fullest at http://backbonejs.org/#Events

Events triggered by the Dashboard object:

Parameter changes - When a parameter changes its value, the event 'cdf

:fireChange' is triggered

Dashboard preInit - When the dashboard finishes running the PreInit method, the event 'cdf cdf:preInit' is triggered.

Dashboard postInit - When the dashboard finishes running the Post init methods, the event 'cdf cdf:postInit' is triggered.

User not logged in - when we detect that an user is no longer logged in, the event 'cdf cdf:loginError' is triggered.

Server error - If a call to the server returns an error, the event 'cdf cdf:serverError' is triggered.

Events triggered by Components:

PreExecution - After preExecution runs, the event 'cdf cdf:preExecution' is triggered.

PostExecution - After postExecution runs, the event 'cdf cdf:postExecution' is triggered.

Error - If an error is triggered during component execution, the event 'cdf cdf:error' is triggered.

Do note that the components have a priority, which will be respected. If you do listen to any of these events and trigger some function (outside of a component), there's no way of knowing when it will run, it may come first, it may come last.

That being said, you may want to bind some orderless logic to some of these events:

require(['cdf/Dashboard.Blueprint', ...,'cdf/Logger'], function(Dashboard, ..., Logger) {

var dashboard = new Dashboard();

var component = ...

...

dashboard.on('cdf cdf:postInit', function(e) {

//I could want to keep a count of all the times my dashboard was visited, for statistical purposes

addOneDashboardInitToStatistic();

});

});

Dashboard types

CDF provides three types of dashboards: Clean, Blueprint and Bootstrap:

The Clean dashboard loads no CSS framework, its just a plain container, you can customize it to the fullest.

require(['cdf/Dashboard.Clean'],...)

The Blueprint dashboard loads the blueprint CSS, so you can use its classes with ease.

require(['cdf/Dashboard.Blueprint'],...)

The Bootstrap dashboard loads the bootstrap framework, increasingly popular and easy to work with.

require(['cdf/Dashboard.Bootstrap'],...)

How to add new Dashboard types

You can add your own dashboard types, to do that you just create a myDashboard.js and inside it something like this:

define(['./Dashboard'], function(Dashboard) {

return Dashboard;

});
myDashboard is basically a Clean dashboard now, you could obviously extend some functionality before returning Dashboard.

define(['./Dashboard'], function(Dashboard) {

return Dashboard.extend({

customize: function() {

//...

}

});

});
myDashboard now has a function called customize, which you could call after the initialization to customize the dashboard further.

You can require it by providing the relative path to it. If myDashboard.js is in the same folder as the template.html (assuming that's what you called the HTML file), you could just use the name myDashboard when requiring:

require(['myDashboard'], function(Dashboard) {

var dashboard = new Dashboard();

dashboard.init();

dashboard.customize();

});

Legacy Dashboard Support

Of course the Dashboards you already have will still be functional, the two types of dashboards will live together and you can use the one you prefer.

In the future however, the default will be require ready dashboards.

You can always make the change to a require dashboard, it's as simple as changing a property in your file.xcdf:

‹cdf›

‹title›Title‹/title›

‹author›Author‹/author›

‹description›Description‹/description›

‹icon›‹/icon›

‹template›template.html‹/template›

‹style›clean‹/style›

‹require›true‹/require›

‹/cdf›
As you can see, the 'require' property is what defines if your dashboard is a require dashboard.

Simple as that, of course if you do want to convert a non-require dashboard to a require one, you will have to change the implementation to use the new API.

Component Development

The process of developing components is a bit different, it's actually a bit simpler once you get used too the whole AMD paradigm.

Lets see a simple example, suppose you have a file called myComponent.js:

//CDF has already several defined modules, you can use them at will

define(["cdf/components/BaseComponent", "cdf/lib/jquery", "cdf/Logger"], function(BaseComponent, $, Logger) {

var myComponent = BaseComponent.extend({

string: "TEST",

getString: function() {

return this.string;

},

writeOnElement: function(selector, text) {

var element = $(selector);

if(element && element.length > 0) {

element.text(text);

} else {

Logger.log("Selector " + selector + " wielded no results");

}

}

});

return myComponent;

});
That's it, you could then require myComponent from your dashboard:

require(["cdf/Dashboard.Blueprint", "myComponent"], function(Dashboard, MyComponent) {

var dashboard = new Dashboard();

dashboard.addComponent(new MyComponent({

//...

}));

dashboard.init();

});
Embedded Capabilities

CDF is easily embedded in any HTML page hosted anywhere really, there is one requisite, you must include in said HTML page, the script for embedding CDF.

This script is asked of CDF, so the real prerequisite, you must make a call to a Pentaho Server running CDF.

Simply include in your HTML page this script tag:

‹script src="http://SERVERNAME/WEBAPPPATH/plugin/pentaho-cdf/api/cdf-embed.js" type="text/javascript"›‹/script›

‹!-- example for a localhost:8080 server: --›

‹script src="http://localhost:8080/pentaho/plugin/pentaho-cdf/api/cdf-embed.js" type="text/javascript"›‹/script›
Note: this implies a valid login in said server

There is one extra setting that needs to be turned on. To properly allow the embedded scenario, which probably requires cross domain requests, the following property needs to be added to the settings.xml file of CDF:

‹allow-cross-domain-resources›true‹/allow-cross-domain-resources›
CDE

Now, the first part may have seemed geeky. That's because it was indeed. CDF is the engine for all the dashboards, so we're very low level. For CDE, however, we're on a much more high level ground. So it's much easier to do things in order to take advantage of those capabilities. And this page spells out all the details.
How to create new "require-ready" dashboards

For now, creating a new dashboard in CDE will not create a RequireJS dashboard by default. In order to do so, you need to check the relevant checkbox in the settings screen when you save a dashboard. The plan is that the default will change to RequireJS by Pentaho 6.0 (October 2015).

Regardless of the default behavior for new dashboards, non RequireJS dashboards will render the same way as before. You can go on editing them without being forced to upgrade to RequireJS, since that involves some changes as described below.

What changes in Dashboard Development from the non RequireJS CDE

Nothing much is the quick but not entirely true answer.

While much does not change, there are some areas that do need some change.

the Dashboards singleton no longer exists

Yes, we finally killed it. Most of the methods you usually called in Dashboards should be available elsewhere. If you're running code in the component context (meaning that this refers to a component), like in preExecution, postExecution and postFetch, most methods will be available in this.dashboard.

Some mention worth exceptions are:

the log method has been migrated to a separate module. That's one of the default modules that every dashboard loads (more on that here). That means Dashboards.log has been replaced with Logger.log

There's a new Utils class with some static methods that were previously in the Dashboards singleton. Examples are escapeHtml, getQueryParameter, objectToPropertiesArray, propertiesArrayToObject are some examples. Check the new class reference here. TODO: Need to complete this when we have the reference published somewhere.

TODO: Are there more we know of ?

Parameters and components no longer create objects in the global scope.

Instead, parameters and components are internal to the dashboard. So if you want to get to a component you need to use the getComponent method in the dashboard object. For parameters, always use the getParameter method.

When debugging, the same rule above applies

From the console, the object render_foo (assuming you have a component called foo in your dashboard) no longer exists. Instead call dashboard.getComponent("render_foo") to get to that component. For convenience, when CDE renders it creates a dashboard object in the global scope. You should use that to get access to the dashboard internals you need.

JavaScript resources

Inline Resource

Inline resources follow the same rules as above. The inline code will be included inside the dashboard module, meaning that you'll have a dashboard object that you can use. Also, you'll have access to all the modules the dashboard already loads.

External Resource developed by you

If you're using an external JavaScript resource developed by you, you should define that as a RequireJS module. That module will be required by the dashboard so that you can use it in the dashboard context.

Example: Imagine you want to define an external resource that exposes an options object you want to use in your dashboard.

That resource will be a RequireJS module. Something like:

define(function() {

return {

option1: 'My Option',

option2: 'Another Option'

};

});

or simply:

define({

option1: 'My Option',

option2: 'Another Option'

});
and you name that resource myProjectOptions.

Under the hood, we'll add the RequireJS module to the dashboard dependency list and we'll call it myProjectOptions. That way, you can access your options object in your dashboard by stating (e.g. in a preExecution):

function f() {

this.htmlObject.text(myProjectOptions.option1);

}
Third Party External Resources

If they are built as RequireJS modules (and most of the JavaScript frameworks right now have that support) it will work exactly the same as the previous case.

If the resource is not exposed as an AMD module, you'll need to develop AMD support for it. Alternatively, the nonamd RequireJS loader plugin can be used to wrap the resource on-the-fly using the define function and make it available as a RequireJS module, according to some shim configurations that must be provided (more on that here).

Default required modules

When CDE renders a dashboard, it requires a collection of base modules by default. That means that a dashboard developer does not need to specifically require them when building custom JavaScript code. The default modules are:

Logger, exposed as Logger

JQuery, exposed as $

Underscore, exposed as _

Moment, exposed as moment

Cdo, exposed as cdo

Utils, exposed as Utils

Custom Component Development and RequireJS vs Legacy Support

CDE supports both legacy and RequireJS dashboards. Legacy custom components can only be used in legacy dashboards and RequireJS custom components can only be used in RequireJS dashboards.

CDE will scan and load the first component.xml configuration file for each unique custom component name it finds in the following locations:

legacy custom components folder resources/custom/components

RequireJS custom components folder resources/custom/amd-components

repository folder Public/cde/components

The component.xml is still used in very much the same way. There are two new attributes in the Implementation tag of the component.xml:

supportsAMD - Might be true or false

If true, the component has a RequireJS implementation, the Implementation/Dependencies and Implementation/Styles tags are ignored and all JavaScript and CSS dependencies will be processed client-side as RequireJS module dependencies when used in RequireJS dashboards.

supportsLegacy - Might be true or false.

If true, the component has a non RequireJS implementation and all it's JS and CSS dependencies are processed server-side using the tags Implementation/Dependencies and Implementation/Styles when used in legacy dashboards.

By default, if you don't supply any of these attributes, CDE will assume that the custom component is a legacy implementation. For any other component type (DataSource, Layout, Parameter, etc.) CDE assumes it supports both implementations, so they can be used by the CDE editor and while rendering CDE dashboards.

CDE doesn't support loading multiple configuration files for custom components with the same name. If users need to have the same custom component support both legacy and RequireJS versions, each version must have a different name.

Another way to bypass this limitation is to have the configuration file only in the legacy custom component folder and set to true the attributes supportsAMD and supportsLegacy. As an example, the custom components shipped with CDE that support both versions only have the configuration file component.xml for the legacy versions, in the legacy custom component folder resources/custom/components.

As before, the Implementation/Code tag of the component.xml will be used to detect where the non RequireJS implementation is. Everything remains the same for non RequireJS custom components.

When a custom component is uploaded to the repository, if it only supports RequireJS dashboards, the Implementation/Code tag is used to configure the RequireJS module path relative to the repository "/public/cde/components" folder. If the uploaded custom component supports both versions, the Implementation/Code tag is used to configure the implementation path for the legacy version, and for the AMD version CDE will search for an implementation file in the same folder as component.xml with the same name as the component's class name (first letter upper case and ending with "Component", e.g. "button" will have the class name "ButtonComponent").

Custom components that are shipped with CDE already have all RequireJS module paths configured in the cde-core-require-js-cfg.js file and the Implementation/Code tag is ignored.

For instance, take the configuration file of the NewSelector custom component:

‹implementation supportsamd="true" supportslegacy="true"›

‹code src="NewSelector.js"›

‹styles›

‹style src="component.css" version="1.0"›NewSelector‹/style›

‹/styles›

‹dependencies›

‹dependency src="component.js" version="1.0"›NewSelector‹/dependency›

‹/dependencies›

‹/code›

‹/implementation›

This info will be used when rendering legacy dashboards, so all JS and CSS dependencies will be included in the rendered HTML page. The RequireJS implementation doesn't require any specific implementation metadata, other than Implementation/Code tag only if the custom component was uploaded to the repository folder so the path can be dynamically built.

So, for RequireJS dashboards the NewSelector implementation is a require module with a path to it's JS implementation file already configured in cde-core-require-js-cfg.js. Its header is:

define([

'cdf/components/UnmanagedComponent',

'cdf/dashboard/Utils',

'cdf/lib/jquery',

'amd!cdf/lib/underscore',

'./NewSelector/views',

'./NewSelector/models',

'css!./NewSelectorComponent'

], function(UnmanagedComponent, Utils, $, _, views, models) {

...

});
All dependencies (css or js) are declared in the module dependency list. That means that custom component dependencies should always be RequireJS modules themselves.

If this is not the case, you may specify a shim that tells RequireJS how to require that specific dependency.

Take the example of the Google Analytics component where the jquery.ga JS resource is not a RequireJS module. We use the module path cde/components/googleAnalytics/lib/jquery.ga to refer to the jquery.ga resource, cde/components points to the RequireJS custom component folder where we can find the jquery.ga.js file inside the sub-folders googleAnalytics/lib. The beginning of the component code manipulates the requireCfg.config to add a new shim definition for jquery.ga using the nonamd RequireJS loader plugin (exposed as 'amd'):

var requireConfig = requireCfg.config;

if(!requireConfig['amd']) {

requireConfig['amd'] = {};

}

if(!requireConfig['amd']['shim']) {

requireConfig['amd']['shim'] = {};

}

requireConfig['amd']['shim']["cde/components/googleAnalytics/lib/jquery.ga"] = {

exports: "jQuery",

deps: {

"cdf/lib/jquery": "jQuery"

}

};

requirejs.config(requireCfg);

The relevant part is the last assignment, where we define a shim for the jquery.ga module. This shim defines both what the module will export (in this case it's the variable jQuery) and what are the modules dependencies, declared on the deps property. Module dependencies essentially allow to control the loading order for these modules. In the current case, we need the cdf/lib/jquery module to load before the jquery.ga module.

The nonamd RequireJS loader plugin will use the shim configuration to dynamically wrap the jquery.ga.js source code as a RequireJS module. This way, using relative paths, the header for the Google Analytics component can declare jquery.ga as a regular dependency:

define([

'cdf/components/BaseComponent',

'cdf/lib/jquery',

'amd!./googleAnalytics/lib/jquery.ga'

], function(BaseComponent, $) {

...

});
Note the amd! prefix, indicating that this module will be required using the nonamd RequireJS loader plugin. That plugin is able to parse and use the shim definition above to wrap on-the-fly JavaScript source code and make it available as a RequireJS module.

For CSS resource loading the RequireJS CSS loader plugin is used, as you can see in the NewSelector example above. Just prefix css! to the CSS file path in the dependency list and load it as if it was a RequireJS module, no shim configurations are needed for using the RequireJS CSS loader plugin.

Require Dashboard Endpoint and Embedding Capabilities

In order to facilitate embedding a CDE dashboard in other apps, we introduced a new endpoint in CDE,

/pentaho/plugin/pentaho-cdf-dd/api/renderer/getDashboard?path=

This endpoint returns a RequireJS module that contains a class for a specific dashboard. You can create new instances of that class, needing only to provide an element id, or an element itself.

The returned class, extends the Dashboard class, adding some new methods:

render()

Renders the dashboard, first setting up the DOM, then adding the components and parameters to the dashboard, and finally calling init()

setupDOM()

Sets up a predefined layout inside its element.

renderDashboard()

Adds the components and the parameters to the dashboard, and then initializes the dashboard

Do note that you need to have CDE embedded for the Dashboard and all its scripts to correctly load. You can easily embed CDE using this endpoint:

/pentaho/plugin/pentaho-cdf-dd/api/renderer/cde-embed.js

There are two ways to embed a CDE dashboard. By using the getDashboard endpoint explicitly:

require([

'/pentaho/plugin/pentaho-cdf-dd/api/renderer/getDashboard?path=/public/plugin-samples/pentaho-cdf-dd/pentaho-cdf-dd-require/cde_sample1.wcdf'

], function(SampleDash) {

var sampleDash = new SampleDash("content1");

sampleDash.render();

});
Or by using the dash! RequireJS loader plugin:

require([

'dash!/public/plugin-samples/pentaho-cdf-dd/pentaho-cdf-dd-require/cde_sample1.wcdf'

], function(SampleDash) {

var sampleDash = new SampleDash("content1");

sampleDash.render();

});
This will require the CDE sample dashboard, creates a new instance of the dashboard and call render, specifying the DOM element with id content1 as the place where the dashboard DOM will be written.

However, you may need to have more control over the whole process. In that case, you shouldn't call render, but:

require([

'dash!/public/plugin-samples/pentaho-cdf-dd/pentaho-cdf-dd-require/cde_sample1.wcdf'

], function(SampleDash) {

var sampleDash = new SampleDash("content1");

sampleDash.setupDOM(); //After this call, the dashboard DOM is created and ready to be used

//Meaning you can call some DOM manipulation logic here before you proceed with the dashboard render

sampleDash.renderDashboard(); //This call, will add the components and parameters to the dashboard, and then effectively render it

});
When embedding multiple dashboards, you might want to connect them using the API. The Dashboard API (TODO: link here) gives you a lot of methods that you can use to connect the dashboards. One of the more primary connections should be linking the change in a parameter in a dashboard to another parameter in another dashboard. So, if parameter A changes in dashboard A, you'd want parameter B to change in dashboard B. We leverage the event library in the underscore library and a parameter change in a CDF dashboard triggers on of these events. So, this link can be achieved with the following code:

require([

'dash!/public/plugin-samples/pentaho-cdf-dd/pentaho-cdf-dd-require/cde_sample1.wcdf'

], function(SampleDash) {

//First we create two instances of the same dashboard giving them distinct DOM elements of our webpage

var sampleDash = new SampleDash("content1");

sampleDash.render();

var sampleDashFoo = new SampleDash("content2");

sampleDashFoo.render();

sampleDash.on("cdf productLine:fireChange", function (evt) { //Now we say that every time the parameter productLine is changed in the sampleDash instance

sampleDashFoo.fireChange("productLine", evt.value); //We want to fire a change for the same parameter in the second dashboard

});

});
There is one extra setting that needs to be turned on. To properly allow the embedded scenario, which probably requires Cross Origin Resource Sharing, the following property needs to be added to the settings.xml files of CDE and CDF:

‹allow-cross-domain-resources›true‹/allow-cross-domain-resources›
Have fun!

More...

Show more