JS Engine

The JS Engine is a framework used by Gambio GX3 e-commerce platform for handling JavaScript operations easily and consistently. It features useful libraries and components which improve re-usability and and boost UI development.

The main concept of JS Engine is that JavaScript code is divided into small modules that are bound directly to HTML elements with the use of "data-" attributes. The JavaScript files are then parsed from the engine, are automatically loaded and finally executed.

The core principles are that every module should be as simple as possible and it should always serve a single purpose within the application context. Being said, modules are divided into four predefined collections based on their functionality type:

  • Controllers
  • Widgets
  • Extensions
  • Compatibility

Each one of these collections contain modules that have the same purpose in the application, something that improves structural organization. The existing collections are described in detail in the following sections.

JS Engine has the ability to load modules from various locations of the application, something that's really important for modular projects where code may lie in various directories. Namespaces define the root directory from which modules are loaded. The shop system has already comes with some predefined namespaces admin, template and common directories.

The engine is already loaded in every shop system page and can be used directly without further configuration. The following example demonstrates the conversion of a plain table into a datatables instance with the use of a single data attribute:

<table data-gx-widget="datatable"> 
  <thead>
    <tr>
      <th>Column 1</th>
      <th>Column 2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
  	  <td>Cell 1</td>
  	  <td>Cell 2</td>
    </tr>
  </tbody>
</table>

When the page is loaded JS Engine will automatically load the "datatable" widget from the "gx" namespace which will convert the table into a jQuery DataTable instance without writing a single line of JS code! Of course real world problems are more complex than this example. The following sections will showcase the capabilities of JS Engine and how it can be used in order to organise and reuse existing code efficiently.


Defining Namespaces

Namespaces define the location of modules in the shop system. Their definition is done with "data-" attributes and must be included in the rendered HTML. JS Engine will parse all the page namespaces and register them before loading the their modules.

<body data-my_custom_name-namespace="admin/javascript/my_custom_folder">...</body>
<!-- OR -->
<div data-my_custom_name-namespace="admin/javascript/my_custom_folder">...</div>
<!-- OR -->
<span data-my_custom_name-namespace="admin/javascript/my_custom_folder">...</span>

The "data-my_custom_name-namespace="admin/javascript/my_custom_folder" attribute will lead to the creation of a "my_custom_name" namespace whose modules will be loaded from "admin/javascript/my_custom_folder". Namespace paths must always be relative to shop system root directory and must not contain the module collection directories. Namespace definition may be added in any place of the rendered HTML but always before DOM is ready. Hence the use of JavaScript for namespace definition is prohibited.

Notice: Namespace names must be unique in the application which means that no other namespace must exist with the same name in the same page.


Collections

JS Engine modules are divided into different collections depending their purpose. Each one of these collections are automatically assigned to new namespaces. They improve module semantics, structure, HTML readability.

Widgets

Widgets define reusable UI components that do not add domain logic into the page. Vendor libraries can be often wrapped by widget modules for easier integration.

Example:

<!-- The following element will become a datetimepicker. -->
<input type="text" data-gx-widget="datetimepicker" />

Extensions

Extensions are JS Engine modules that extend existing widgets with additional functionality which is optional and may or may not apply in any occasion. For example the addition of the data-gx-extension="link" will enable the link functionality into any element.

Example:

<label data-gx-extension="link" data-link-url="http://gambio.de">Navigate To Gambio.de</label>

Controllers

Implementing new pages often require specific functionality and domain logic. These modules must not be reused and they must only work for a single page. Multiple controllers can cooperate in the same page with the use of events.

Example:

<!-- JS Engine will automatically include the my_controller.js file. -->
<div class="container" data-gx-controller="my_controller">...</div>

Compatibility

Compatibility modules were introduced during the Admin layout refactoring phase and they are mixed-purpose modules that are used only for the implementation of a compatibility mode. The separation reason is that theis sole purpose is to refurbish legacy pages which will be gradually removed.

Example:

<!-- JS Engine will automatically include the compatibility_module.js file. -->
<div class="container" data-gx-compatibility="compatibility_module">...</div>

Modules

JS Engine module API is progressive and lightweight. Each JavaScript file must define a single module which serves a single purpose. By convention modules use an underscore separator and are written in lower case. Module and file names must be the same, except from the exception ('my_module' must be stored in my_module.js). Module dependencies can be defined as an array and JS Engine will make sure that they are loaded before initializing the module. Finally a main function must return the module object and provide a private environment for the module code.

Example:

// The simplest JS Engine module definition.
namespace_name.controller.module('controller_name', [], function(data) { 
  return {
    init: function(done) {
  	  alert('Hello World');
  	  done();
    }
  };
});

This example demonstrates the simplest possible module definition. When the page loads the module will alert the message "Hello World" and then inform JS Engine that it's done by calling the "done" function. It is a controller module and belongs to the "namespace_name" namespace.

Example:

<div data-namespace_name-namespace="admin/javascript/namespace_name"> 
  <span data-namespace_name-controller="controller_name">Bound Element<span>
</div>

After the namespace definition JS Engine will be able to load any modules located in the target directory.


Libraries

Commonly used code can be placed in utility libraries that can be used by any module. Libraries objects defined under "jse.libs" and contain reusable methods. They must be defined as module dependencies so that JS Engine loads them before module get initialized. The shop system contains many libraries for common operations such as the DataTable adapter library or the loading spinner functionality.

Example:

jse.libs.lib_name = jse.libs.lib_name || {}; 

(function(exports) {
	
	'use strict'; 
	 
	 var _privateMethod = function() {
	    // ...
	 };
	 
	 exports.publicMethod = function() {
	    // ... 
	 };
	 
})(jse.libs.lib_name); 

Modules can require core libraries that reside in JSEngine/build/libs directory with their filename, while any other
library requires the use of a full file URL. At this point JS Engine can also load CSS stylesheets as well, making it easier to use requested files on demand.

namespace_name.extensions.module(

'module_name',
 
[
	'datatable', // Core library (only the file name is required).
	namespace_name.source + '/libs/my_lib', // URL without the extension.
], 

function(data) {
	return {
		init: function(done) {
		    jse.libs.datatable.create($('#random-table'), {}); 
          	jse.libs.publicMethod();
          	done();
		}
	};
}); 

Examples

As simple as it may seem JS Engine is a progressive framework. Real life problems are much more complex but with the correct setup they can be easily tackled with high quality code and well structured components. This section contains some examples that demonstrate various solutions for different scenarios.

Get Parameters Trough HTML Attributes

HTML

<button data-namespace_name-widget="widget_name" data-widget_name-text="Hello World!"></button>

Module

namespace_name.widgets.module('widget_name', [], function(data) {
	var $this = $(this);
	var module = {}; 
		
	module.init = function(done) {
		$this.on('click', function() {
			alert(data.text); // data.text value comes from the HTML markup and contains "Hello World!".
		});
		done(); 
	};
	
	return module;
});

Load Custom Vendor Library

HTML

<button data-namespace_name-widget="widget_name"></button>

Module

namespace_name.widgets.module(
	'widget_name', 
	
	[
		jse.core.config.get('appUrl') + '/JSEngine/build/vendor/qtip2/jquery.qtip.min.css',	
		jse.core.config.get('appUrl') + '/JSEngine/build/vendor/qtip2/jquery.qtip.min.js'	
	], 
	
	function(data) {
		var $this = $(this);
		var module = {}; 
			
		module.init = function(done) {
			$this.qtip();
			done(); 
		};
		
		return module;
});

Module Communication Through Events

HTML

<div data-namespace_name-controller="parent_controller"> 
  <button data-namespace_name-controller="child_controller">Click Me</button>
</div>

Module

// child_controller.js
namespace_name.controllers.module(
	'child_controller', 
	
	[], 
	
	function(data) {
		var $this = $(this);
		var module = {}; 
			
		function _onButtonClick() {
			$this.trigger('child_controller:update', ['Click']); // Trigger a custom event for other modules.  
		}
		
		module.init = function(done) {
			$this.on('click:update', _onButtonClick); 
			done(); 
		};
		
		return module;
});

// parent_controller.js
namespace_name.controllers.module(
	'parent_controller', 
	
	[], 
	
	function(data) {
		var $this = $(this);
		var module = {}; 
			
		function _onChildControllerUpdate(event, data) {
			console.log('Child Controller Event:', data); // Data Contains "Click" 
		}
		
		module.init = function(done) {
			$this.on('child_controller:update', _onChildControllerUpdate); 
			done(); 
		};
		
		return module;
});

Dynamic Module Initialization

In some cases modules need to be initialized dynamically. HTML markup may be renewed or the initial markup might not
contain the required data attributes. This can be easily done by manually calling the initialization method in the container element as shown in the example below.

$('.container-element')
  .html('<input type="checkbox" />')
  .attr('data-gx-widget', 'single_checkbox'); 
gx.widgets.init($('.my-container')); 

In this snippet an input checkbox is created dynamically and then gets converted to a "single_checkbox" widget. The "gx.widgets" collection needs to be initialized once again but this time only within "container-element" object and not the whole page.


About This Document

This documentation contains reference and examples for further information on available extensions and widgets and the core engine libraries. It is highly preferable that you use JS Engine features in your implementations because they are tested and will keep your pages up to date with the latest features.

Notice: Every section of JS Engine except from the the core libraries need to use underscore separators for the module names because the engine supports the inclusion of custom variables passed through the HTML to the modules in the following format data-my_controller_name-title="Some Title" and the my_controller_name cannot be neither CamelCase "MyControllerName" nor dash-separated "my-controller-name". So try to keep the names as short as possible.

It will be helpful to look in existing namespaces and modules in order to get an idea of how the files are structured. The admin namespace is the best place to get ideas from (admin/javascript/engine).


About The Engine

Copyright: Gambio GmbH © 2018

License: GPLv2

Current Version: v1.6

Website: www.gambio.de