Controller

A controller is a JavaScript piece of code that can be associated to a view, and it manages its data, presentation and the usage/interaction logic. Through a controller, it's possible to customize all aspects of a component's lifecycle since it's early initialization up to the disposal of the component itself. This can be done by implementing component's lifecycle callbacks.

Loading a controller

A controller can be loaded programmatically using the zuix.loadComponent(..) method, or it can be loaded directly in the HTML page, by using the attributes ctrl z-load="<component_id>" on the host element:

<div ctrl z-load="path/of/controller"></div>

when the ctrl attribute is present, only the .js controller file will be loaded, and the host element (a div in this case) will be set as the view of the loaded controller. If the ctrl attribute is not present, then the view files will also be loaded from .html + .css files, unless differently specified in the controller's onInit() callback.

After loading, a ComponentContext object will be created as instance of the loaded component. A reference to a ComponentContext can be obtained with the zuix.context(..) method:

let myControllerContext;
zuix.context(hostElement, (ctx) => {
  myControllerContext = ctx;
}); 

or through the ready callback in the loading options.

Implementation

A controller can be implemented in a <component_name>.js file using one of the following code templates:


class ComponentName extends ControllerInstance {

  /* Life-cycle callbacks */
  onInit() { }
  onCreate() { }
  onDispose() { }
  onUpdate(target, key, value, path, old) { }

}
'use strict';
function ComponentName() {
    
  /* Life-cycle callbacks */
  this.init = function() { };
  this.create = function() { };
  this.dispose = function() { };
  this.update = function(target, key, value, path, old) { };

}

Within the controller's scope, the context object this, is the ContextController instance through which is possible to access the view template's fields, querying its DOM, handling input events and triggering output events, exposing public interface members, and other common component's implementation tasks.

Using zuix.js ES5 class format it is also possible to have static and private members. See the example ES5 class.

Lifecycle callbacks

onInit() ( <controller>.init() )

The onInit method gets called right after the JavaScript controller has been loaded and before any other resource is loaded. This function can be used to get, set component's options, or to load additional dependencies.

onInit() {
  const opts = this.options();
  // enable view style encapsulation
  opts.encapsulation = true;
  // do not inherit styles from parent
  opts.resetCss = true;
  // do not load template's CSS file
  opts.css = false;
  // custom option
  if (opts.myCustomOption === 'some-value') {
    // TODO: handle custom option
  }
}
this.init = function() {
  const opts = this.options();
  // enable view style encapsulation
  opts.encapsulation = true;
  // do not inherit styles from parent
  opts.resetCss = true;
  // do not load template's CSS file
  opts.css = false;
  // custom option
  if (opts.myCustomOption === 'some-value') {
    // TODO: handle custom option
  }
};

At this stage it's also possible to change the ready status of the component to false (this.context.isReady = false). This can be useful in case the component will load other dependencies before becoming fully operational and setting back this.context.isReady = true. During this "not ready" interval, the class .not-ready will be added to the component's view so that it can be used to customize how to component will look like while it's still loading.

onCreate() ( <controller>.create() )

The onCreate method gets called right after all component's resources have been loaded. At this stage it is already possible to access the component's view and the data model. This method is also employed to register input event listeners, to declare members to expose publicly and to declare members available in the view's scripting scope.

onUpdate() ( <controller>.update(target, key, value, path, old) )

This method gets called anytime a bound field of the data model is updated.

onDispose() ( <controller>.dispose() )

This method gets called right before the component is unloaded and disposed, and it's employed to clear timers and correctly dispose other resources that are not automatically handled by zuix.js.

Inline implementation of a component

A component can also be implemented inline, directly in the HTML page, as shown in the following inline component template (view + controller):

<div z-view="path/of/component-name">
  <!-- component's view template content -->
  <h1>Hello World!</h1>
</div>

<style media="#path/of/component-name">
  /* styles definitions of this component's view */
  h1 { color: deeppink; }
</style>

<script>
class ComponentName extends ControllerInstance {
    onCreate() {
      console.log('Component created.');
    }
    // ...
}
zuix.controller(ComponentName)
    .for('path/of/component-name');
</script>

A component declared inline can be loaded as any other component:

<div z-load="path/of/component-name"></div>

or, if a custom element tag has been defined:

<component-name></component-name>

Common tasks

Consider this simple view

<div z-load="default">
    
    <div #message></div>
    
</div>

where, in the controller's code, this is the ContextController instance:

See the ContextController API for a list of all available properties and methods.

Examples

As an example, the following controller's code, is the implementation of a Material Design Light button:


/**
 * MdlButton class.
 * @constructor
 * @this {ContextController}
 */
class MdlButton extends ControllerInstance {

  onCreate() {

    const $view = this.view();
    const options = this.options();
    const type = options.type || 'raised';
    $view.addClass('mdl-button mdl-js-button mdl-button--' + type + ' mdl-js-ripple-effect');
    if (options.class) {
      $view.addClass('mdl-button--' + options.class);
    }

  }

}
// file: "controllers/mdl-button.js"
'use strict';
/**
 * @class MdlButton
 * @constructor
 * @this {ContextController}
 */
function MdlButton() {

  this.create = () => {

    const $view = this.view();
    const options = this.options();
    const type = options.type || 'raised';
    $view.addClass('mdl-button mdl-js-button mdl-button--' + type + ' mdl-js-ripple-effect');
    if (options.class) {
      $view.addClass('mdl-button--' + options.class);
    }

  }

}
// file: "controllers/mdl-button.js"

This controller just adds the required CSS classes to turn the host element into an MDL button. The same thing could be similarly done for Bootstrap, Materialize.CSS, or any other CSS UI Framework.

<a ctrl z-load="@lib/controllers/mdl-button"> With </a>
... or ...
<a> Without </a>

With ... or ... Without

So, in the create lifecycle callback, the controller can access the view element and the component's options, and in this example the controller recognizes two option fields, type and class, that control the button appearance:

<a ctrl z-load="@lib/controllers/mdl-button"
   :type="'fab'" :class="'mini-fab'">
  <i class="material-icons">mail</i>
</a>

<a ctrl z-load="@lib/controllers/mdl-button"
   :type="'flat'">
    Flat
</a>

<a ctrl z-load="@lib/controllers/mdl-button">
    Regular
</a>
mail
Flat
Regular

In this other example of an MDL menu, the main div container loads the mdl-menu controller, and contains the menu's items list and a button to activate the menu itself:

<div ctrl z-load="@lib/controllers/mdl-menu" z-lazy="false"
     :behavior="menuButtonBehavior" class="visible-on-ready">

  <!-- the menu is defined as a simple UL list -->
  <ul>
    <li>Menu option 1</li>
    <li>Menu option 2</li>
  </ul>

  <!-- the menu's FAB button -->
  <a ctrl z-load="@lib/controllers/mdl-button"
     :type="'fab'" :class="'mini-fab colored'">
    <i class="material-icons">add</i>
  </a>

</div>

this time, the menu controller, besides adding the required classes for the Material Design menu, it also intercepts when the MDL menu opens/closes, so that it can trigger the menu:show and menu:hide custom events. This controller also improves the MDL menu by adding auto-positioning feature, so the menu will slide up or down, based on the actual position of the button.

  • Menu option 1
  • Menu option 2
menu

Custom events triggered by the component, can then be used, like in this example, to animate the button with a behavior: when the menu:show event occurs, the behavior's code will rotate the button element (a) by 135 degrees and will set the icon "close". When instead, the menu:hide event occurs, the button element is rotated back to 0 degrees and the icon is set back to "menu".

<script>
menuButtonBehavior = {
  'menu:show': function() {
      this.find('.material-icons').html('add')
          .css({
            transform: 'rotate(135deg)',
            transition: 'transform .2s ease-in'
          });
  },
  'menu:hide': function() {
      this.find('.material-icons').html('menu')
          .css({
            transform: 'rotate(0)',
            transition: 'transform .2s ease-in'
          });
  }
}
</script>

Global event hooks

While component events are local to each instance, global events have only one global listener (hook) and this kind of event is usually employed to get notified about certain task progress, or to transform input data.

There are three types of global events:

Loader's events can be used to determine whether zuix.js is actually loading components or not, and, for instance, to show a visible feedback to the user.

Component's allocation events can be used to process the view's HTML template and style before they are attached to the DOM, and also to process the view's DOM after the component is loaded and before the component is actually created.

Custom component's global events can be used to allow notification or transformation for a type of components.

For an example on how to use this kind of events, see zuix.hook(..) in the API documentation page.

Global resources and singleton components

Like the examples in these pages, where some components depend on MDL library, a controller implementation might depend on some external libraries. To correctly deploy such a component then, a list of its dependencies should be provided as pre-requisites, so that these dependencies can be added to the page hosting these components.

Alternatively, a component can declare all resources it requires in order to work properly, so that if not already loaded in the page, these resources will be automatically loaded along with the component.

For this purpose the zuix.using(..) and ContextController.using(..) methods can be used to load common dependencies such as utility scripts, stylesheets or utility/service controllers that are accessible application-wide. Library shortcuts can also be used in the URL path.

Some examples:

// Load library from CDN if not already included in the document
zuix.using('script', 'https://some.cdn.js/moment.min.js', function(resourcePath, hashId) {
  // can start using moment.js
});
// Load styles from CDN if not already included in the document
if (!zuix.$.classExists('.animate__animated .animate__bounce')) {
  zuix.using('style', '@cdnjs/animate.css/4.1.1/animate.min.css', function(resourcePath, hashId) {
    // AnimateCSS animation classes loaded
  });
}
if (!zuix.$.classExists('[layout]')) {
  const flexLayoutUrl = '@cdnjs/flex-layout-attribute/1.0.3/css/flex-layout-attribute.min.css';
  zuix.using('style', flexLayoutUrl, function(resourcePath, hashId) {
    // Flex Layout Attribute loaded
  });
}
// Load a singleton component (application-wide service)
myService = null; 
zuix.using('component', 'controllers/my-service-api', function(resourcePath, ctx) {
  // component loaded
  myService = ctx;
  myService.publicMethod1();
  myService.publicMethod2('test');
  // ...
});
Events and Active→Refresh
CLI / Templates
GitHub logo
JavaScript library for component-based websites and applications.