DHTMLX Optimus Framework

DHTMLX Optimus is a micro framework for building DHTMLX-based apps.

The framework enforces consistent application structure by breaking a monolithic application into a set of reusable classes. As a result, each part of an app can be developed and tested independently and used in various combinations.

DHTMLX Optimus is a fully client-side solution. There aren't any special requirements to the server. You can use any data REST backend (PHP, Nodejs, .Net, Java, etc.)

General Idea

DHTMLX Optimus is based on the modern approaches of web development:

  • ES6 classes
  • JavaScript modules
  • Webpack module bundler

The framework uses BabelJS for code processing, so the resulting app will work even in older browsers, supported by DHTMLX (IE8+)

Each module describes a part of UI (a single view). You can use normal DHTMLX code inside of the module to initialize a single component. Top level modules can combine multiple views into a layout and so on. The app itself can be used as a view in some other application.

The framework provides a common event bus to allow communication between views and some other helpers for common DHTMLX tasks.

How to Start

Grab the starter package from github. After unzipping the package install dependencies and run local dev server by using:

npm install
npm run server

If everything works fine, you will see a simple app built with Optimus framework:

Other useful commands

// add watcher which will rebuild JS and CSS files on code changes 
npm run watch
 
// rebuild JS file in the "codebase" folder
npm run codebase
 
// lint the sources
npm run lint
 
// build the production package
npm run package

"package.json" file notes

Pay attention to the "package.json" file in the root of your app's project. The main fields is important, it is the primary entry point to your program. This should be the name of the app's module.

Application

Structure of an Application

  • index.html file - the start page of the application
  • sources/app.js file - contains the application's configuration
  • sources/views folder - includes files with interfaces' descriptions
  • sources/less folder - contains the app's styles
  • codebase/ folder - includes static files used in the application (images, icons, styles, data files, etc.). The content of this folder is added by the build.

DHTMLX files

By default, index.html includes a free version of DHTMLX from our CDN. If you are using DHTMLX PRO, place the DHTMLX files into the "codebase" folder and adjust links in the index.html.

CDN

<link href="//cdn.dhtmlx.com/site/dhtmlx.css" rel="stylesheet"></link>
<script src="//cdn.dhtmlx.com/site/dhtmlx.js"></script>

Local

<link href="codebase/dhtmlx.css" rel="stylesheet"></link>
<script src="codebase/dhtmlx.js"></script>

Application Initialization

The code is straightforward, just call the app constructor and use the render() function to render the app:

<script type="text/javascript">
    var app = new MyApp({});  // takes app's config as a parameter
    app.render();             // renders the app
</script>

App's Configuration

The app's configuration file is called app.js. It contains the main class of the application:

app.js

import "less/MyApp.less";
import {DHXApp} from "dhx-optimus";
 
import {MainLayout} from "views/mainlayout.js";
 
class MyApp extends DHXApp{
    render(){
        this.show(MainLayout);
    }
}
 
window.MyApp = MyApp; // here we add our app into the global window object

There are 3 mandatory principles here:

  • class which extends DHXApp must be defined
  • class must have the render() method which defines the initial view to show
  • class must be registered in the global scope, to be accessible on an HTML page

In the above example we have included the following dependencies:

  • The file that contains app's styles (MyApp.less in our example)
  • The DHXApp class from the "dhx-optimus" library
  • The view with the main layout of the application (views/mainlayout.js in our example)

Application API

DHXApp class provides the next API for the application class:

  • show(view, cell) - shows the specified view in a cell of the app
    • view - (object) the view to show
    • cell - (object) a reference to the cell to render the view inside
  • imagepath(component) - sets the path to the folder with images of the particular component
    • component - (string) the name of the component to set image path to
  • attachEvent(name, handler) - registers event handler
    • name - (string) name of the event
    • handler - (function) user-defined event handler
  • callEvent(name, params) - calls an event
    • name - (string) the event name
    • params - (array) an array of the event related data
  • addService(name, object) - adds a service for the app (read more in the Services section)
    • name - (string) - the service's name
    • object - (object) - the service's config
  • getService(name) - returns a service added to the app by its name (read more in the Services section)
    • name - (string) - the service's name
      • returns the service's object
  • destroy() - destroys the application

Working with Views

Defining Views

The view's configuration is similar to that of an application. A new view class is inherited from the DHXView class. The render() method is used to draw a view.

import { DHXView } from "dhx-optimus";
export class GridView extends DHXView {
    render() {
        this.ui = this.root.attachGrid();
        this.ui.setInitWidths("*,200,80,80");
        this.ui.init();
    }
}
  • this.ui - a reference to the created DHTMLX component. It's recommended to use in order to ensure the view's destruction after closing
  • this.root - a reference to the parent cell

In the above example we have set a view with a grid inside and specified some config properties for it. You can create any number of views by this means and use all of them in your application.

We recommend to have a single view per file and name file the same as the view, e.g. GridView => views/grid.js.

Layout View

You can initialize layouts in the same way:

import { DHXView } from "dhx-optimus";
import { TreeView } from "./tree.js";
import { GridView } from "./grid.js";
 
export class MyView extends DHXView {
    render() {
        this.ui = this.root.attachLayout({ pattern:"2U"});
        this.show(TreeView, this.ui.cells("a"));
        this.show(GridView, this.ui.cells("b"));
    }
}

As you can see, we can use the show() API to render some other view inside of a layout's cell.

By combining those types of views, you can create a static UI of any complexity. When necessary, you can add dynamics - change a part of UI with a different one:

import { DHXView } from "dhx-optimus";
import { TreeView } from "./tree.js";
import { GridView } from "./grid.js";
 
export class MyView extends DHXView {
    render() {
        this.ui = this.root.attachLayout({ pattern:"2U"});
        this.addSlot("left",  this.ui.cells("a"));
        this.addSlot("right", this.ui.cells("b"));
 
        var grid = this.show(MasterGridView, "left");
        grid.ui.attachEvent("onRowSelect", (id) => {
            if (id == -1)
                this.show(DashBoardView, "right");
            else
                this.show(DetailsView, "right");
        })
    }
}

The above code shows different views in the cell "b", based on the selected row in the grid. You can see that the code uses addSlot() API to set a name for the cell with dynamic content.

Connecting Views

In the above snippet we have used grid.ui.attachEvent() to register an event handler for the element in the child view. This is a bad practice, since we get a tightly coupled resulting code which can be easily broken.

There is a better approach - the usage of the global event bus. After attaching an event in one view, you can call it in another one.

Let's consider an example. We have a dataview called "Countries" in the countries.js file and a grid named "Cities" in cities.js. We want the grid to display cities related to the country selected in the dataview.

In such a case we can define a dataview in the following way:

import { DHXView } from "dhx-optimus";
 
export class CountriesView extends DHXView {
    render() {
        this.ui = this.root.attachDataView({
            type: {
                template: tpl
            }
        });
 
        this.ui.attachEvent("onItemClick",  id => this._countrySelect(id));
    },
    _countrySelect(id){
        const country = this.ui.get(id);
        this.callEvent("onCountrySelect",[{
            cities:country.cities
        }]);
    }

Here on selecting some item, the code calls the onCountrySelect event, which can be caught in any other view.

import { DHXView } from "dhx-optimus";
 
export class CitiesView extends DHXView {
    render() {
        this.ui = this.root.attachGrid();
        this.ui.setHeader("Name,Population");
        this.ui.init();
        this.attachEvent("onCountrySelect", e =>this._setCities(e.cities));
    }
    _setCities(cities){
        this.ui.clearAll();
        this.ui.parse(cities);
    }
}

When a country is selected in the dataview, the _setCities() method is called and data in the grid is refreshed. So the two views are now connected.

Event handlers will be automatically detached on view destruction.

Destroying Views

Using slots

DHTMLX Optimus automatically destroys a view, when a different view is shown in the same slot.

addSlot() allows adding a slot (named area) into a cell. The content of the slot can be changed/removed without affecting the rest of the cell's area.

For example, we have a layout with 2 cells. We want to dynamically change the content of the cell "a". Firstly, we use the addSlot() method to add a slot named "details" into the cell "a".

// adding a slot called "details" into the cell "a"
this.addSlot("details", top.cells("a"));

Then, the show() method is used to render desired views in the added slot. It takes the name of the view that will be displayed in the slot and the name of the slot as parameters.

// rendering a grid view in the "details" slot
this.show(GridView,"details");

Then you can show a different view in the slot and it will be rendered instead of the previous one:

// rendering a data grid view in the "details" slot
this.show(DataGridView,"details");

While the slot content is re-rendered, the GridView view in the "details" slot is destroyed and replaced with DataGridView, while the rest content of the cell "a" will remain the same.

Destroy() API

If you will ever need a custom destruction code, just redefine the destroy() method of the view to have a custom on destroy logic:

export class MyView extends DHXView {
    render() {
        this.window = new dhtmlXWindows();
        ...
    }
    destroy(){
        /*any custom code here*/
        this.window.unload();
        super.destroy();
    }
}

Main UI Destruction

The main UI, the reference to which is stored in this.ui will be destroyed automatically, you don't need to use a custom code to clear it.

View API

As it was mentioned above, views are created on the base of the DHXView class. This class possesses public API methods listed below:

  • show(view, cell) - shows the specified view in a cell
    • view - (object) the view to show
    • cell - (object) a reference to the cell to render the view inside
  • addSlot(name, obj) - adds a slot (named area) into a cell. Useful for creating cells with dynamic content
    • name (string) - the name of the slot
    • object (object) - a reference to the cell to add a slot inside
  • refresh() - refreshes the view
  • attachEvent(name, handler) - adds a user-defined handler to an event
    • name - (string) name of the event
    • handler - (function) user-defined event handler
  • callEvent(name, params) - calls an event
    • name - (string) the event name
    • params - (array) an array of the event related data
  • addService(name, obj) - add a service for a view (read more in the Services section)
  • getService() - returns a service added to the view by its name (read more in the Services section)
  • render() - renders a view on the page
  • imagepath(component) - sets the path to the folder with images of the particular component
    • component - (string) the name of the component to set image path to
  • destroy() - destroys a view

Special Types of Views

Working with Windows

A Window view should be attached to the layout, in other aspects it is the same as other views:

export class MyDialog extends DHXView{
    render(){
        this.ui = this.root.dhxWins.createWindow({
            // window config
        });
        this.ui.attachEvent("onClose", () => {
            //destroy view on window closing
            this.destroy();
        })
    }
}

Such a view can be used inside of another view as follows:

export class MyView extends DHXView{
    render(){
        this.ui = this.root.attachToolbar();
 
        this.ui.attachEvent("onClick", (id) => {
            ...
            if(id == "add_connection"){
                this.show(MyDialog, this.root);
            }
        });
    }
}

Handlebar Templates

In many cases some complex HTML code should be rendered in a view. It's not convenient to do it with the help of the JavaScript code. In the case of DHTMLX Optimus, you can use Handlebar templates for such a task.

For example, let's have a look at the file called templates/about.html that contains the description of a file explorer application:

<div id="intro_text" class="intro">
    <div style="margin: 30px;">
        <h1>dhtmlxFileExplorer Demo Application</h1>
        <p>The purpose of this demo is to illustrate the possibility of building 
        Windows File Explorer-like application using DHTMLX library. 
        The following components were used:</p>
        <ul>
            <li></li>
            <li>Key</li>
        </ul>
        ...
    </div>
</div>

To show this template in an app, we will use the view like this:

import { DHXView } from "dhx-optimus";
import about from "templates/about.html";
 
export class AboutView extends DHXView {
    render() {
        this.ui = this.root.attachHTMLString(about({
            key:"value",
            anotherKey:"anotherValue"
        }));
    }
}

The above code imports an HTML template and attaches it to the root cell. During rendering the code sends a hash of parameters which will be placed inside of the template.

Loading/Saving Data

You can use normal DHTMLX API to load data into any component.

We recommend to store Toolbar, Form and Menu configuration as a part of the view file, and load external data only for data components such as Grid, Tree, TreeGrid, DataView and Chart.

<p class='snippet'> "countries.js"</p>
import { DHXView } from "dhx-optimus";
 
export class CountriesView extends DHXView {
    render() {
        this.ui = this.root.attachDataView({
            //data loading
            url:"data.php"
        });
 
        //data saving
        var dp = new dataProcessor("save.php");
        dp.setTransactionMode("REST");
        dp.init(this.ui);
    }
}

Working with Back-end (PHP, Node, etc.)

When you are using an app in the Developer mode (npm run server), it will use its own server, so all calls to PHP or any other backend scripts need to be routed. This is controlled by two parameters:

  • In webpack.config.js:
  devServer:{
    proxy: {
      '/server/*': pkg.localURL
    }
  },

Instructs all calls to the "server" folder to be routed to some other server.

  • And in package.json:
"localURL": "http://127.0.0.1/dhtmlx/demos/optimus-start/",

localURL defines the full web URL of the backend server.

Localization

DHTMLX Optimus allows creating multi-language applications. Localization is implemented on the base of the Polyglot.js library.

To enable localization for the app's interface, you need to import Polyglot into the main file of your application (sources/app.js). After that create an instance of the Polyglot class which you will use for translation:

npm install --save node-polyglot
import {DHXApp, DHXLocale} from "dhx-optimus";
import Polyglot from "node-polyglot";
import en from "locale/en.js";
import de from "locale/de.js";
 
class MyApp extends DHXApp{
    render(){
        this.locale = new DHXLocale(Polyglot, { en, de });
    }
}

Later in any view you can use this.app.locale to access text translations.

Storing Locales

Locales are stored in separate JS files in the locales folder.

Inside of the locale's file a common JS object stores a collection of the key:value pairs corresponding to the text strings and their translations. For example:

// sources/locales/fr.js
export default const fr = {
    // dataview
    "Name" : "Nom",
    "User" : "Utilisateur",
    "Description" : "Description",
}

It will set the necessary language and take translation phrases from helpers/langs.js.

// views/grid.js
export class GridView extends DHXView{
    render(){
        const _ = this.app.locale.helper();
 
        this.ui = this.root.attachGrid();
        this.ui.setHeader([
            _("Flag"), _("ID"), _("Status"), _("Subject"), _("Manager")     
        ]);
        this.ui.init();
    }
}

The English locale is set by default, you can change the default language in the main view using the setLang() method:

class TicketApp extends DHXApp{
    render(){
        this.locale = new DHXLocale(Polyglot, { en, de });
        this.ui = this.show(TopView);
 
        this.attachEvent("onLangChange", lang => {
            this.setLang(lang);
            this.ui.refresh();
        });
    }
}

Services

DHTMLX Optimus provides an easy way to use Dependency Injection container. The app and all views have addService() and getService() API which can be used to define some shared module. It can be used to share the state of components without directly exposing them.

For example, you can have a TreeView:

class TreeView extends DHXView {
    render(){
        var sId = null;
 
        this.ui = this.root.attachTree();
        this.ui.attachEvent("onClick", (id) => sId = id }); // store selected item
 
        this.addService("DocumentTree", {
            selected:() => sId
        })
    }
}

Now it's possible to get the id of the selected item from any other view like this:

var id = this.getService("DocumentTree").selected();

If a view which has registered some module is destroyed, all registered services are deleted as well.

Other Helpers

Image Path Helper

For some components it's necessary to specify the path to the folder with images. In order not to set the full path each time, the framework provides a useful helper.

You can apply the helper inside of the regular setImagePath() method of the main UI, like this: this.app.imagepath("componentName").

For example, to set path to the images' folder of the Tree component, use the code as in:

this.ui.setImagePath(this.imagepath("tree"));

Deploying an Application

To deploy your app to the live server, use the following NPM command:

npm run build

It will create a "dist" folder with all necessary files. Just push those files on the production server.

After that you can call npm run deploy to synchronize the dist folder to the target server (path to the target server is configured inside of gulpfile.js).

Using App as a View inside of Other Apps

With DHTMLX Optimus, you can combine several applications and make them work as modules of one large application. The API is the same as for working with views, just use the this.show command.

import {DHXView} from "dhx-optimus";
 
import {App1} from "app1.js";
import {App2} from "app2.js";
 
export class TopView extends DHXView{
    render(){
        let top = this.ui = this.root.attachLayout("2U");
 
        let app1 = this.show(new App1(config),top.cells("a"));
        let app2 = this.show(new App2(config),top.cells("b"));
    }
}

You can use different applications or create multiple instances of a single app.

Related Materials

Back to top