Dmitry Sheiko's

Web Development Blog

Scalable JavaScript Design Pattern

17 October 2011

Studying open source solutions you will find variety of different JS code designs. The question is which to follow? Which will serve better for your requirements? But what the requirements? As for me, it is usually events within modules or globally in document which should be handled. Handlers affect document DOM-tree, frequently, to update a module view or render a new component on the page. It brings me to the point: I need a code design where for every module as well as for document there is a scope. It contains sections to define key nodes, to subscribe handlers on particular events and rendering methods when it is necessary. Let’s now see how it can be on the following example:


JS application example


The page contains 'intro' module. The module has background picture and links on the toolbar. Using these links user changes module picture (background). Besides, the change of this image affects another module. Let’s say it changes title of the window.

Pretty simple, isn’t it? The HTML would be:


<!doctype html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>JS Application Design: Pic1</title>
    <link href="all.css" rel="stylesheet" media="all" />
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
    <script src="example.js" type="text/javascript"></script>
</head>
<body>
    <div class="module intro">
        <div class="toolbar">
            <ul>
                <li title="Pic1" id="pic1">link 1</li>
                <li title="Pic2" id="pic2">link 2</li>
                <li title="Pic3" id="pic3">link 3</li>
            </ul>
        </div>
    </div>
</body>
</html>

I guess the only what needs any comments is attributes of "li" element. Here we will need item "title" value when changing window title and "id" value says which image to show in module. So, moving to example.js we define our local scope:


(function() {
}());

Now we have isolated our code and don’t so care of any other scripts included on the page. But it means we can’t access JQuery inside as well. I do like jQuery, so I’m going to pass the reference inside the scope:


(function( $ ) {
})( jQuery );

There we can specify scope constants. E.g.:


(function( $ ) {
const PAGE_TITLE_PREF = 'JS Application Design : ';
// Though, to avoid troubles in IE, rather user VAR to introduce constants as well
})( jQuery );

Then what about entities? I see here viewport with a module. By second glance, here is an application which has one module. I like this one.

We hardly will have instances of the application, but very likely instances of the module. Thus, application will be a static object and the module – dynamic one within the first one.


(function( $ ) {
var App = function() {
  return {
      init : function() {
      },
      //  Module
      Intro : function() {
      }
    };
  }();
})( jQuery );

Module contains constants, private properties and methods, public properties and methods:


Intro : function() {
      // Private properties
      var  _a = null,
           _b = null;
      return {
        init : function(settings) {
        }, 
      };
  }

Since the module here is a widget in essence, we follow YUI pattern. Module has public property node which is a static object with all key node references we are dealing with in module. It has public methods:

  • Init – module initialization
  • renderUI – defines the key nodes and controls rendering when it’s required
  • syncUI – subscribes module event handlers

Handlers here belong to the only module, so we put them in a private static object.


Intro : function() {
      // event handlers
      var _handler = {
            onclick : function(e) {
                // e.data is this
                e.data.node.boundingBox.css('background-image', 'url(./'
                    + $(this).attr('id') + '.jpg)');
                // Example of communication module to module, module to application
                $(document).trigger('intro-onchange', [this]);
            }
      };
      return {
        node : {
           boundingBox : null,
           toolbar : null
        },
        init : function(settings) {
            if (settings.boundingBox === undefined) {
                throw "boundingBox must be specified";
            }
            $.extend(this.node, settings);
            this.renderUI();
            this.syncUI();
        },
        renderUI : function() {
            this.node.toolbar = this.node.boundingBox.find('div.toolbar');
        },
        syncUI : function() {
            this.node.toolbar.find('li').bind('click.intro', this, _handler.onclick);
        }
      };
  }

You must have noticed that the module object receives settings as an argument. In this case the only parameter we need is the node where to bind the module in DOM-tree. I’ve already mentioned Progressive Enhancement once and now I need only to remind that page components are built statically when page is generated and afterwards if it is enabled they are enriched by JS. So Js modules are to be bound to already built nodes. In our case, when DOM-tree is ready, we bootstrap App (App().init), which creates instance of Intro module and binds it to given node.


// Local namespace
(function( $ ) {
// Application namespace
var App = function() {
  return {
  init : function() {
    // Invoke the first instance of Intro
    var intro = new App.Intro();
    intro.init({boundingBox: $('div.intro')});
  },
  //  Module
  Intro : function() {
      //...
  }
  };
}();
// Document is ready
$(document). bind('ready.app',App.init);

})( jQuery );

Ok, now if we click on a link (list item) the module image changes. Let’s see now how we can communicate from intro module to another one. We just fire a global event (it can be local event of the module) passing all required arguments to it:


$(document).trigger('intro-onchange', [this]);

Application would be the addressee component, which is subscribed for the event. So, we extend it the same way as Intro with handles container and syncUI function.


// Local namespace
(function( $ ) {
// Namespace constants
var PAGE_TITLE_PREF = 'JS Application Design : ';
// Application namespace
var App = function() {

      // Application event handlers
     var _handler = {
          intro : {
            onchange: function(e, node) {
               document.title = PAGE_TITLE_PREF + $(node).attr('title');
            }
          }
      };

    return {
      init : function() {
        App.syncUI();
        // Invoke the first instance of Intro
        var intro = new App.Intro();
        intro.init({boundingBox: $('div.intro')});
      },
      syncUI : function() {
         // Application subscribes for a custom event
        $(document).bind('intro-onchange', _handler.intro.onchange);
      },
      //  Module
      Intro : function() {
          // event handlers
          var _handler = {
                onclick : function(e) {
                    // e.data is this
                    e.data.node.boundingBox.css('background-image', 'url(./'
                        + $(this).attr('id') + '.jpg)');
                    // Example of communication module to module, module to application
                    $(document).trigger('intro-onchange', [this]);
                }
          };
          return {
            node : {
               boundingBox : null,
               toolbar : null
            },
            init : function(settings) {
                if (settings.boundingBox === undefined) {
                    throw "boundingBox must be specified";
                }
                $.extend(this.node, settings);
                this.renderUI();
                this.syncUI();
            },
            renderUI : function() {
                this.node.toolbar = this.node.boundingBox.find('div.toolbar');
            },
            syncUI : function() {
                this.node.toolbar.find('li').bind('click.intro', this, _handler.onclick);
            }
          };
      }
    }
}();

// Document is ready
$(document).bind('ready.app',App.init);

})( jQuery );

Now the same, but using JSA


It is still not very object-oriented. We see here instances with the similar properties and some of similar behavior. It feels that there should be an abstract class which they inherit. Let’s put it on a diagram:



For every widget instance we always have node collection and have to specify in it all the nodes we are dealing with inside boundingBox. If we had an abstract class which fills in the collection automatically based on a map (HTML_PARSER property), we would have our widgets more explicit. All this logic would go to the WidgetAbstract, and would be inherited by our widgets. But how to extend classes with private scope like in the pattern shown above? Prototyping won’t work in this case and you will be hardly able to invoke the constructor. So, I’ve written a simple jQuery plugin which solves the problem for me. It provides BaseAbstract and WidgetAbstract in the YUI3 fashion, that can be extended by $.jsa.extend function.


aWidget = function(settings) {
      return $.jsa.extend({
        name : 'aWidget',
        HTML_PARSER : {
        },
        init : function() {
        },
        renderUI : function() {
        },
        syncUI : function() {
                    }
      }, $.jsa.WidgetAbstract, settings);
  };
var instance = aWidget ({boundingBox: $('...')}).getInstance();

When creating a widget, you bind it to a DOM-node. You pass the node as a part of settings object, which is a class parameter. $.jsa.BaseAbstract, emulating constructor in getInstance method, will save settings as a public property. It checks for init, renderUI, syncUI methods and invokes those which are found. It does this also for every abstract class inherited by the widget. So, it finds init() method of $.jsa.WidgetAbstract and iterates HTML_PARSER to populate node collection. So, boundingBox is added to node property automatically from settings. When HTML_PARSER defined (e.g. HTML_PARSER : { nodeReference : 'CSS-selector', ..} ), it appends node collection with the corresponding nodes. Thus, our generic widget gets lighter. Now let’s see how the application changed when using jquery.jsa plugin.


// Local namespace
(function( $ ) {
// Namespace constants
var PAGE_TITLE_PREF = 'JS Application Design : ';
// Application namespace
var App = function() {
      // Application event handlers
      var _handler = {
          intro : {
            onchange: function(e, node) {
               document.title = PAGE_TITLE_PREF + $(node).attr('title');
            }
          }
      };
      return {
          init : function() {
            App.syncUI();
            // Invoke the first instance of Intro
            App.Intro({boundingBox: $('div.intro')}).getInstance();
          },
          syncUI : function() {
             // Application subscribes for a custom event
            $(document).bind('intro-onchange', _handler.intro.onchange);
          }
      }
}();
  //  Module
  App.Intro = function(settings) {
      // event handlers
      var _handler = {
            onclick : function(e) {
                // e.data is this
                e.data.node.boundingBox.css('background-image', 'url(./'
                    + $(this).attr('id') + '.jpg)');
                // Example of communication module to module, module to application
                $(document).trigger('intro-onchange', [this]);
            }
      };
      return $.jsa.extend({
        name : 'Intro',
        HTML_PARSER : {
            toolbar : 'div.toolbar'
        },
        syncUI : function() {
            this.node.toolbar.find('li').bind('click.intro', this, _handler.onclick);
        }
      }, $.jsa.WidgetAbstract, settings);
  };


// Document is ready
$(document).bind('ready.app',App.init);

})( jQuery );


Now you can download the package with the example here.