Andre Jorge

RequireJS in CTools Dashboards

Blog Post created by Andre Jorge Employee on May 15, 2018

Intro

This post aims to explain how CDF and CDE support the RequireJS JavaScript file and module loader, what concepts changed and the key points to help the development of dashboards that support AMD.

 

RequireJS

Here are some things you should know about RequireJS:

  • RequireJS implements the Asynchronous Module Definition specification
    • This basically means you load RequireJS and its configurations into your application, nothing else. The rest of the resources your application needs will be loaded asynchronously via RequireJS, this guarantees you only load what you use.
  • Each resource must be a properly defined module.
  • It's a good practice to not name modules (define unnamed modules) and use relative paths to specify dependencies as much as possible.

When requiring a module, if no extra configuration is added, you will require it using the path relative to the resource depending on it, e.g.:

  • folder
    • file.js
  • folder2
    • file2.js

To require file2.js from file.js you would do:

require(["../folder2/file2"], function(file2) { ... });

Note: you can read all about this at http://requirejs.org/

 

CDF

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();

  // add logic here

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

  dashboard.init();

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

  Logger.log("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. The 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("logging stuff...");

});

Note: If you will use Logger a lot, consider requiring it along 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 more about it at http://backbonejs.org/#Events.
Below you will find a list of some of the main events used in CDF. Along with each specific dashboard/component event, CDF always triggers a "cdf" event (each event is separated by a white space).

  • Events triggered by the dashboard object:
    • Parameter changes - When a parameter changes its value, the events "cdf <parameterName>:fireChange" are triggered
    • Dashboard preInit - When the dashboard finishes running the PreInit method, the events "cdf cdf:preInit" are triggered.
    • Dashboard postInit - When the dashboard finishes running the Post init methods, the events "cdf cdf:postInit" are triggered.
    • User not logged in - when we detect that an user is no longer logged in, the events "cdf cdf:loginError" are triggered.
    • Server error - If a call to the server returns an error, the events "cdf cdf:serverError" are triggered.
  • Events triggered by components that extend from the Unmanaged component:
    • PreExecution - After preExecution runs, the events "cdf cdf:preExecution" are triggered.
    • PostFetch - After postFetch runs, the events "cdf cdf:postFetch" are triggered.
    • Render - After triggerAjax runs successfully, the events "cdf cdf:render" are triggered.
    • PostExecution - After postExecution runs, the events "cdf cdf:postExecution" are triggered.
    • Error - If an error is triggered during component execution, the events "cdf cdf:error" are triggered.

If a user needs to react to all of these main events he can simply listen to the "cdf" event.
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. The following example listens to the dashboard "cdf:postInit" event and executes the function addOneDashboardInitToStatistic every time the event is triggered:

require([

  "cdf/Dashboard.Blueprint",

  "cdf/Logger",

  ...

], function(Dashboard, Logger, ...) {

  var dashboard = new Dashboard();

  ...

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

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

    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. As an example on how to do it, create a JavaScript file named myDashboard.js and inside it write something like this:

define(["cdf/Dashboard.Clean"], function(Dashboard) {

  return Dashboard;

});

The file myDashboard.js defines an anonymous AMD module that returns a Clean dashboard and you can obviously extend some functionality before returning Dashboard.

define(["cdf/Dashboard.Clean"], function(Dashboard) {

  return Dashboard.extend({

    customize: function() {

      ...

    }

  });

});

All dashboards created using the custom type myDashoard now have 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 the file 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 relative path ./myDashboard when requiring it:

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 the require property in the XCDF file:

<?xml version="1.0" encoding="UTF-8"?>

<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, to do so you must include RequireJS and its configurations for embedding CDF. These are available in the CDF endpoint plugin/pentaho-cdf/api/cdf-embed.js, you must make this request to the Pentaho Server running CDF. Simply include the following in your HTML page:

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

<!-- example for a localhost:8080 server: -->

<script type="text/javascript" src="http://localhost:8080/pentaho/plugin/pentaho-cdf/api/cdf-embed.js"></script>

Note: this implies a valid login in the server.

 

Regarding configuring the plugin configuration files, there are a couple of settings that needs to be turned on. To properly allow the embedded scenario, which might require cross domain requests, the following property needs to be edited in CDF's settings.xml file:

<!--

    allow-cross-domain-resources: Flag indicating cross-origin resource sharing

    accepted values are: true | false

-->

<allow-cross-domain-resources>false</allow-cross-domain-resources>

 

<!-- A comma separated list of allowed domains for a cross-origin resource sharing -->

<cross-domain-resources-whitelist><!--intentionally left blank --></cross-domain-resources-whitelist>

In order to access common-ui resources, the file pentaho-solutions/system/pentaho.xml also needs changes to the following sections:

<!--

  cors-requests-allowed:

    Flag indicating if cross-origin requests are allowed or not.

    accepted values are: true | false

-->

<cors-requests-allowed>false</cors-requests-allowed>

 

<!--

  cors-requests-allowed-domains:

    Comma separated list of domains allowed to do cross-origin requests to the server.

  Example:

    http://domainA.com, http://localhost:1337

-->

  <cors-requests-allowed-domains><!-- allowed domains here --></cors-requests-allowed-domains>

 

CDE

Creating a new dashboard in CDE will create a RequireJS compatible dashboard by default. In order to switch to a legacy dashboard you need to check the relevant checkbox in the settings screen and save the dashboard.

Regardless of the default behavior for new dashboards, legacy 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 logging methods have been migrated to a separate module with id cdf/Logger. That's one of the default modules that every dashboard loads (more on that here). That means Dashboards.loghas been replaced with Logger.log
  • There's also a new module with id cdf/dashboard/Utils that contains some of the static methods that were previously in the Dashboards singleton. Examples are escapeHtml, getQueryParameter, objectToPropertiesArray and propertiesArrayToObject are some examples. Check the new class reference here. TODO: Need to complete this when we have the reference published somewhere.

 

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

There are two ways of adding JavaScript resources. These can be added as inline code which will be inserted within the context of the dashboard, or the resource can be added as an external JavaScript file. When uploading external JavaScript files into the repository, if the resource is not added directly into the dashboard (it might be a dependency of another module), the prefix cde/resources/ should be used to access the repository. For example, if the JavaScript dependency is in the repository path /public/dashboard/files/plugin.js it can be added as a dependency of another AMD module by using the module id cde/resources/public/dashboard/files/plugin:

define([

  "cdf/lib/jquery",

  "cde/resources/public/dashboard/files/plugin"

], function($, Plugin) {

  return $.parseJSON(Plugin.getSomethingFromServer());

});

Another thing to take into account is that when adding an external JavaScript resource in the CDE editor, the resource name will be the name of the variable accessible in the dashboard context. For example, if we add the previous plugin.js file and name it Plugin the following code will be produced when rendering the dashboard:

require([

  ...,

  "cdf/lib/jquery",

  "cde/resources/public/dashboard/files/plugin"

], function(..., $, Plugin) {

  /* the value plugin.js' module returns is accessible via the Plugin variable here */

});

If the external JavaScript resource is doesn't return a value (e.g. just adds some extra functionality to the jquery object) or is a CSS resource, the CDE dashboard renderer will exclude the input variable producing:

require([

  ...,

  "cdf/lib/jquery",

  "cde/resources/public/dashboard/files/plugin"

], function(..., $) {

  /* one less var in the dashboard context  */

});

Note: Do not add more than one JavaScript external resources with the same name.

 

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 http://redmine.webdetails.org/projects/cde/wiki/RequireJS#Custom-Component-Development-and-RequireJS-vs-Legacy-Support).

 

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:

  1. legacy custom components folder resources/custom/components
  2. RequireJS custom components folder resources/custom/amd-components
  3. 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 supportsLegacy="true" supportsAMD="true">

  <Code src="NewSelector.js"/>

  <Styles>

    <Style version="1.0" src="component.css">NewSelector</Style>

  </Styles>

  <Dependencies>

    <Dependency src="component.js" version="1.0">NewSelector</Dependency>

  </Dependencies>

</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.

 

Using Custom Components from third party plugins

In order to be able to use custom components from third party plugins, the plugins must declare the main RequireJS configuration file in their plugin.xml file. For example, in order to be able to use the RequireJS version of the AppBuilderPluginCard component, the RequireJS configuration file appBuilder-require-js-cfg.js was added to the plugin.xml content:

<?xml version="1.0" encoding="UTF-8"?>

<plugin title="AppBuilder" loader="DEFAULT" name="AppBuilder">

...

<external-resources>

  <file context="requirejs">content/appBuilder/resources/appBuilder-require-js-cfg.js</file>

</external-resources>

This configuration file defines the base path for the App Builder plugin components and the path for the main JavaScript file of the AppBuilderPluginCards component AppBuilderPluginCardComponent.js is being referenced in the end of the last line (without the file extension):

...

  prefix = requirePaths["AppBuilder/components"] =

    CONTEXT_PATH + "api/repos/AppBuilder/resources/amd-components";

...

  requirePaths["AppBuilder/components/AppBuilderPluginCardComponent"] =

    prefix + "/AppBuilderCards/AppBuilderPluginCardComponent";

...

CDE renderer will use the value of the plugin title "AppBuilder" from the plugin.xml file to build RequireJS module ids in order to use the plugin's custom components. This is why the module ids start with "AppBuilder" in the configuration file. You can also see that "AppBuilder/components" is referencing the amd-components folder created within the AppBuilder plugin filesystem.

Finally, as read in the previous section, we only need to copy the folder containing the RequireJS version of the AppBuilderPluginCard component into the folder amd-components and because we are supporting both legacy and AMD versions we need to edit the following line of the legacy component's component.xml file as such:

...

  <Implementation supportsLegacy="true" supportsAMD="true">

...

 

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=<path to dashboard>

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

 

Note: 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 less verbose 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 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, same as above for CDF (including the changes to the file pentaho.xml).

<!--

    allow-cross-domain-resources: Flag indicating cross-origin resource sharing

    accepted values are: true | false

-->

<allow-cross-domain-resources>false</allow-cross-domain-resources>

 

<!-- A comma separated list of allowed domains for a cross-origin resource sharing -->

<cross-domain-resources-whitelist><!--intentionally left blank --></cross-domain-resources-whitelist>

Outcomes