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.)
DHTMLX Optimus is based on the modern approaches of web development:
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.
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:
// 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
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.
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.
<link href="//cdn.dhtmlx.com/site/dhtmlx.css" rel="stylesheet"></link>
<script src="//cdn.dhtmlx.com/site/dhtmlx.js"></script>
<link href="codebase/dhtmlx.css" rel="stylesheet"></link>
<script src="codebase/dhtmlx.js"></script>
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>
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:
In the above example we have included the following dependencies:
DHXApp class provides the next API for the application class:
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();
}
}
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.
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.
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.
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.
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();
}
}
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.
As it was mentioned above, views are created on the base of the DHXView class. This class possesses public API methods listed below:
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);
}
});
}
}
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.
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);
}
}
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:
devServer:{
proxy: {
'/server/*': pkg.localURL
}
},
Instructs all calls to the "server" folder to be routed to some other server.
"localURL": "http://127.0.0.1/dhtmlx/demos/optimus-start/",
localURL defines the full web URL of the backend server.
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.
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();
});
}
}
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.
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"));
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).
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.