Subscribe via Feed

Writing Client-Side Javascript for Re-Use, Part II

Jeremy Hodge, Mar 14, 2010 12:00:07 AM

This is the second post in a series on reuseable javascript objects. If you have not read it yet, start at the first post here.

Now, let's continue on by creating a new, blank database somewhere on your Domino webserver to serve as your new re-usable javascript code repository. A simple URL denoting reusable javascript objects is probably best. For example, at ZO, I've created a database titled "ZetaOne Javascript Repository" in the root of the data directory called zojs.nsf.  Give anonymous reader access to the database so it can be referenced from any application. In that new database, lets create a new client side javascript library. with the name "com/ZetaOne/widget/listControl.js".  The name is important.  The script library name needs to match what we are going to define our new class as. I'll explain why later.

Lets get started by entering a few basic lines to define the actual class:


        dojo.declare("com.ZetaOne.widget.listControl", dijit._Widget, {
            create: function(params, srcNodeRef) {

            destroy: function() {


This is the very basic definition if our new class. the first line "dojo.provide" plugs our new class into the dojo runtime, so dojo knows it has been loaded. This is important, as different objects can reference the same class multiple times, but it will only be loaded one time to save download time and bandwidth, and to prevent existing data from getting over-written by one module reloading an existing module.

The next line "(function(){" creates whats known as an anonymous function that allows us to define our new "class". Which is where the third line "dojo.declare" comes in. That block of code creates a new "class" called com.ZetaOne.widget.listControl that inherits from dijit._Widget (dijit._Widget is the base class in dojo from which most all UI controls are derived. We won't get into why we're inheriting from this class in this post, just know that we have basically now extended dijit._Widget).  Our new class also has two new members "create" and "destroy" which are just like standard constructors and destructors in languages that implement a real class.

We can add additional member objects, variables and functions by declaring them just like the create and destroy functions are defined.  For reference, here is the entire implementation of the com.ZetaOne.widget.listControl object as it has been implemented as a class object. We won't get into the nitty gritty of what is going on with this class, but it's here as an example. In a later post, we'll get deeper into this actual object to describe the mechanations at work.


        dojo.declare("com.ZetaOne.widget.listControl", dijit._Widget, {
            create: function(params, srcNodeRef) {
                    if (typeof srcNodeRef == 'undefined') {
                        console.error('com.ZetaOne.widget.listControl requires a DOM node!');
                        return false;
                    } else
                        this.domNode = srcNodeRef;
                    this.selectMultiple = dojo.attr(this.domNode, 'selectMultiple');                            
                    this.groupClass = dojo.attr(this.domNode, 'groupClass');
                    this.itemClass = dojo.attr(this.domNode, 'itemClass');
                    this.selectedClass = dojo.attr(this.domNode, 'selectedClass');
                    this.lastSelectedClass = dojo.attr(this.domNode, 'lastSelectedClass');                    
                    this.selectMultiple = (this.selectMultiple == null) ? "true" : eval(this.selectMultiple);
                    this.groupClass = (this.groupClass == null) ? 'zoListControlGroup' : this.groupClass;
                    this.itemClass = (this.itemClass == null) ? 'zoListControlItem' : this.itemClass;
                    this.selectedClass = (this.selectedClass == null) ? 'zoListControlSelected' : this.selectedClass;
                    this.lastSelectedClass = (this.lastSelectedClass == null) ? 'zoListControlLastSelected' : this.lastSelectedClass;
                    this._itemClassQuery = '.' + this.itemClass;
                    this._groupClassQuery = '.' + this.groupClass;
                    this._selectedClassQuery = '.' + this.selectedClass;
                    this._lastSelectedClassQuery = '.' + this.lastSelectedClass;
                    if (dojo.isIE)
                        document.onselectstart = function() { return false; }
                    else {            
                    // associate onclick events
                    var selectables = dojo.query(this._itemClassQuery);
                    for (i=0;i                        dojo.addClass(selectables[i], this.groupClass);
                        this.connect(selectables[i], 'onclick', this._captureClick);
            destroy: function() {
                    dojo.forEach(this._connects, this.disconnect);
            pop: function() {
                    var nodes = dojo.query(this._groupClassQuery + this._selectedClassQuery);
                    for (i=0;i                        nodes[i].parentNode.removeChild(nodes[i]);
                    return nodes;                    
            push: function(nodes, select) {
                    if (select) nodes.addClass(this.selectedClass);
                    for (i=0;i                        this.domNode.appendChild(nodes[i]);
                        this.connect(nodes[i], 'onclick', this._captureClick);
            connect: function(
                    /*Object|null*/ obj,
                    /*String|Function*/ event,
                    /*String|Function*/ method){
                var d = dojo;
                var dc = dojo.connect;
                this._connects[] = dc(obj, event, this, method);
                return this._connects[];
            disconnect: function(/*Object*/ handle){
                // summary:
                //        Disconnects handle created by this.connect.
                //        Also removes handle from this widget's list of connects
                // tags:
                //        protected
                var i = dojo.indexOf(this._connects, handle);
                this._connects.splice(i, 1);
            // PRIVATE MEMBERS    
            _selectItem: function(selection, e) {
                    var selNode = dojo.byId(selection);
                    if (((!e.ctrlKey) && (!e.shiftKey)) || !this.selectMultiple) {
                      dojo.query(this._groupClassQuery + this._itemClassQuery).removeClass(this.selectedClass);
                      dojo.addClass(selNode, this.selectedClass);
                    } else if ((e.ctrlKey) && (!e.shiftKey))
                      if (dojo.hasClass(selNode, this.selectedClass))
                        dojo.removeClass(selNode, this.selectedClass)
                        dojo.addClass(selNode, this.selectedClass);
                    else if (e.shiftKey) {
                      var idx = dojo.indexOf(selNode.parentNode.childNodes, selNode);
                      var lastIdx = dojo.indexOf(selNode.parentNode.childNodes, dojo.query(this._groupClassQuery +
                              this._groupClassQuery + this._lastSelectedClassQuery)[0])
                      if (idx == lastIdx) {
                          // user just shift clicked the same node again, don't do anything
                          return true;
                      if (!e.ctrlKey) {
                        // clear all previous selections first, re-add lastSelected, then process on
                        dojo.query(this._groupClassQuery + this._selectedClassQuery).removeClass(this.selectedClass);
                        dojo.query(this._groupClassQuery + this._selectedClassQuery).addClass(this.selectedClass);
                        dojo.addClass(selection, this.selectedClass);
                      if (lastIdx == -1) {  // the last selected was not in the same parent, so we can't shift click, so just reset the focused nodes and select the single node ...
                        dojo.query(this._groupClassQuery + this._selectedClassQuery).removeClass(this.selectedClass);      
                        dojo.addClass(selNode, this.selectedClass);
                      } else {
                          if (idx < lastIdx) {
                            var el = selNode;
                            for ( ; lastIdx >= idx; idx++) {
                              if ((el.nodeType == 1) && (dojo.hasClass(el, this.groupClass)))
                                dojo.addClass(el, this.selectedClass);
                              el = el.nextSibling;
                          } else if (idx > lastIdx) {
                            var el = selNode;
                            for ( ; lastIdx <= idx; idx--) {
                              if ((el.nodeType == 1) && (dojo.hasClass(el, this.groupClass)))
                                dojo.addClass(el, this.selectedClass);
                              el = el.previousSibling;
                    dojo.query(this._groupClassQuery + this._lastSelectedClassQuery).removeClass(this.lastSelectedClass);
                    dojo.addClass(selNode, this.lastSelectedClass)
            _disableselect: function(e) { return false; },
            _reEnable: function() { return true; },
            _captureClick: function() {
                var e = (!arguments[0]) ? window.event : arguments[0];
                var selection = ( ? : e.srcElement;
                this._selectItem(, e);

Copy and paste that code into your "com/ZetaOne/widget/listControl.js" library, then save and close it.
Now in a different database, let's create an XPage that consumes this object. First create an XPage with the following code, making sure to set dojo ParseOnLoad and dojoTheme to true in the XPage's All Properties:





There are a couple of things to point out in the above code. First, notice the first xp:div tag has a dojoType that matches the class we defined earlier "com.ZetaOne.widget.listControl".  This instructs dojo to create an object out of this HTML element based on that class.  The tags are used to pass parameters to the constructor function of the class. Additionally, if you have been following along in this series so far, you might notice that we are don't have code in the XSP markup to handle the onClick events when the user selects an item. This has been automated in the class, and we'll review in a future post.

To make this a working model, you'll also need to assemble the Cascading Style Sheet from the previous two posts, place it in the database, and include it as a reference on this page.
We now have a complete implementation of the listControl object in an application, BUT we still need to tell the browser to actually load the javascript class that we created in our new javascript repository.

We'll cover that in Part 3.

0 responses to Writing Client-Side Javascript for Re-Use, Part II