Advertisement

Writing Modular JavaScript

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

When writing an entire web application in JavaScript, it's very important to have it well-organized; maintaining a spaghetti-coded project with only cause you headaches and nightmares. In this tutorial, I'll show you how to modularize your code to make it easier to manage large JavaScript projects.


Introduction

Recently, I re-watched one of my favourite JavaScript presentations: Scalable JavaScript Application Architecture, by Nicolas C. Zakas. I found the modular JavaScript patterns he recommended particularly fascinating, so I decided to give it a try. I took the theoretical snippets of code from his slides, expanded and customized them, and came up with what I’ll show you today. I can hardly claim to be an expert at this: I’ve only been thinking about this method of writing JavaScript for just over a week. But hopefully I can show you how you might put what Zakas was promoting into practice, and get you thinking about coding this way.


A Word About the Accompanying Screencast

There's a screencast that goes along with this tutorial; it's rather long (almost 2 and a half hours, actually), so here's table of content to help you navigate it; I've given each section a title "slide" to make it easier to find the beginning of each section.

  • 0:00:00 - Beginning
  • 0:05:15 - Building the Modules
  • 0:40:05 - Building the Sandbox
  • 1:04:39 - Building the Core
  • 1:36:55 - Building the interface (HTML & CSS)
  • 1:51:08 - Debugging!
  • 2:02:27 - Building the Core (Dojo Edition)
  • 2:18:47 - Discussing the Benefits

Nicolas Zakas’ Presentation

If you haven’t watched it recently, you should watch Zakas’ presentation before going on; most of what I’ll be showing you will be easier to understand with this under your belt:

The Presentation


Note: this is not the original presentation that this tutorial was based on, but a more recent version of the same talk.

The Slides


The Summary

So, in summary, a good JavaScript application will have four layers. From bottom up, these are as follows:

  • The Base: this would be your JavaScript framework, like jQuery, if you were using one. Or, you can write your own.
  • The App Core: this layer is in charge of hooking everything together and running your web app. It also provides an equalization layer for the next layer up (the sandbox); that might seem redundant once you get into it, and you’ll be wondering why we don’t just let the sandbox talk directly to the base. However, having it run everything through the core first enables us to easily swap out the base for a different framework. Then, we only have to change the middle-man functions in the core.
  • The Sandbox: this is the mini-API—and the only API—that each part of our web app has access to. Once you’ve created a sandbox you’re happy with, it’s very important that the outward interface doesn’t change in any way. This is because you’ll have many modules depending on it, and changing the API would mean going through every module and updating it—not something you want to do. Of course, you can modify the code inside the sandbox’s methods (as long as they still do the same thing), or add functionality.
  • The Modules: these are the real working in our app. Each module is a self-contained piece of code that runs a single aspect of the web app. In the way I’ve previously written web apps, all the module code is intertwined in one spaghetti-like mess, and when one piece is missing, the whole app crashes. When everything it put neatly in modules (with interactions provided through the sandbox), a missing piece doesn’t cause any errors.

To explain this pattern, we’ll be creating a mini online store (well, just the front end). This really displays the benefit of modules, because an online store has many distinct parts:

  • a product panel
  • a shopping cart
  • a search box
  • a way to filter products

Each of these pieces is clearly a separate part of the page, but all of them do need to interact with other modules—either as an initiator or as a reciever. We’ll see how all this works!

All right, enough theory already! Let’s start coding!


The Building Blocks: Modules

When I started this project, I didn’t have a core or sandbox to work off. So, I decided to start by coding the modules, because this would give me an idea of what the modules needed to be able to access via the sandbox.

When creating a module, I used a pattern that’s very close to the sample code Zakas showed:

CORE.create_module("search-box", function (sb) { 
    return { 
        init : function () {}, 
        destroy : function () {} 
    }; 
});

As you can see, we’re using the method create_module on the CORE to register this module with the web app (of course, we haven’t created the core yet, but we’ll get there). This function will take two parameters: the name of the module (in this case, “search-box”) and a function that returns the module object. What I’ve shown here is the most basic module possible. Notice a few things about the creator function: first, it has a single paramter, which will be an instance of our sandbox (when we look at the sandbox, we’ll see why an instance is better than having all modules access the same sandbox object). This sandbox object is the only connection the module has to the “outside world.” (Of course, as Zakas said, there are really no technical constraints to stop you from accessing the base or core directly from within the module; you just shouldn’t do it.) As you can see, this function returns an object, which is our module object. At the very least, that module just have an init method (used when we start up the module) and a destroy method (used when we shut the module down).

So, let’s actually build a real module.


Full Screencast



The Search Box Module

CORE.create_module("search-box", function (sb) { 
    var input, button, reset; 
 
    return { 
        init : function () {}, 
        destroy : function () {}, 
        handleSearch : function () {}, 
        quitSearch : function () {} 
    }; 
});

You should understand what’s going on here: our module will use three variables: input, button, and reset. Besides the two required module functions in the return object, we’re creating two search-related functions. I don’t think there’s any reason they have to be part of the returned module object; they could just as easily be declared above the return statement and referenced via closure. Let’s dive into each one of these functions:

init : function () { 
    input = sb.find('#search_input')[0]; 
    button = sb.find('#search_button')[0]; 
    reset  = sb.find("#quit_search")[0]; 
 
    sb.addEvent(button, 'click', this.handleSearch); 
    sb.addEvent(reset, 'click', this.quitSearch); 
},

First, we’re assigning the three variables that we need. Since we’ll need to be able to DOM elements from our modules, we’ll need a find method on our sandbox. What’s with the [0] at the end, though? Well, our sb.find method returns something similar to a jQuery object: the elements matching the selector are given numbered keys, and then there are some methods and properties. However, we’re only going to need the raw DOM elements, we’re grabbing that (since only one element will have an id, we can be sure we’re only returning one element).

Two of these elements (button and reset) are buttons, and so we’ll need hook up some event handlers. We’ll have to add that to the sandbox as well! As you can see, it’s your standard add-event function: it takes the element, the event, and the function.

How about on the destruction of the module:

destroy : function () { 
    sb.removeEvent(button, 'click', this.handleSearch); 
    sb.removeEvent(reset, 'click', this.quitSearch); 
    input = null; 
    button = null; 
    reset = null; 
},

It’s pretty simple: there will have to be a removeEvent function that undoes the work of addEvent. Then, we’ll just set the three module variables to null.

Those two event listeners reference the search functions. Let’s look at the first one:

handleSearch : function () { 
    var query = input.value; 
    if (query) { 
        sb.notify({ 
            type : 'perform-search', 
            data : query 
        }); 
    } 
},

First, we get the value of the search field. If there’s something in it, we’ll move on. But what should we do? Normally when doing a dynamic search (that doesn’t require a page refresh of ajax request), we’d have access to the panel of products and could filter them appropriately. But our module has to be able to exist with or without a product panel; plus, it’s only link to the outside world is through the sandbox. So here’s what Zakas proposed: we just tell the sandbox (which in turn tells the core) that the user has performed a search. Then, the core will offer that information to the other modules. If there’s one that responds, it will take the data and run with it. We do this via the sb.notify method; it takes an object with two properties: the type of event we’re performing and the data related to the event. In this case, we’re doing a ‘perform-search’ event, and the relevant data is the search query. That’s all the search box module needs to do; if there’s another module that has exposed the ability to be searched, the core will give it the data.

The neat thing to note about this is that this method is completely versatile. The module that will use this event in our example won't do anything Ajax-y, but there's no reason another one module couldn't do that, or search in some completely other way.

The quitSearch method isn’t much more complicated:

quitSearch : function () { 
    input.value = ""; 
    sb.notify({ 
        type : 'quit-search', 
        data : null 
    });                 
}

First, we’ll clear the search box; then, we’ll let the sandbox know that we’re running a ‘quit-search’; in this case, there’s no relevant data.

Believe it or not, that’s the whole search module. Pretty simple, eh? Let’s move on to the next one.

The Filters Bar Module

We want out online store to give users the ability to view products by category. So, let’s implement a filters bar, where the user clicks the category names to show only the items in the given category.

CORE.create_module("filters-bar", function (sb) { 
    var filters; 
    return { 
        init : function () { 
            filters = sb.find('a'); 
            sb.addEvent(filters, 'click', this.filterProducts); 
        }, 
        destroy : function () { 
            sb.removeEvent(filters, 'click', this.filterProducts); 
            filters = null; 
        }, 
        filterProducts : function (e) { 
            sb.notify({ 
                type : 'change-filter', 
                data : e.currentTarget.innerHTML 
            }); 
        } 
    };         
});

This one isn’t very complicated. We’ll need a variable to hold the filters. As you can see, we’re using sb.find to get all the anchors. But what are the chances of all the anchors on the page being filters? Not very good. Once we get to the writing the find method, you’ll see how it only returns the elements within the DOM element corresponding to our module. Then, we’ll add a click event to the filters, which calls the filterProduct method. As you can see, that method simply tells the sandbox about our ‘change-filter’ event, giving it the text of the link clicked at the data (e is the event object, and currentTarget is the element that was clicked.

Of course, destroy simply gets rid of the event listeners.

The Product Panel Module

This one is going to be pretty lengthly and possibly complicated, so hang on tight! We’ll start with a shell:

CORE.create_module(“product-panel”, function (sb) {  
    var products; 
         
    function eachProduct(fn) { 
        var i = 0, product; 
        for ( ; product = products[i++]; ) { 
            fn(product); 
        } 
    } 
    function reset () { 
        eachProduct(function (product) { 
            product.style.opacity = '1';     
        }); 
    } 
    return { 
        init : function () {}, 
        reset : reset, 
        destroy : function () {}, 
        search : function (query) {}, 
        change_filter : function (filter) {}, 
        addToCart : function (e) {} 
 
    };       
});

Take a moment to look over this; there isn’t a lot that’s different from our other modules; there’s just more of it. The main difference is that I’ve used two helper functions, created outside the returned object. The first one is called eachProduct, and as you can see, it simply takes a function and runs it for each item in the products list. The other is a reset function, which we’ll understand in a moment.

Now let’s look at the init and destroy functions.

init : function () { 
    var that = this; 
    products = sb.find('li'); 
    sb.listen({ 
            'change-filter'  : this.change_filter, 
            'reset-fitlers' : this.reset, 
            'perform-search' : this.search, 
            'quit-search'    : this.reset 
        }); 
    eachProduct(function (product) { 
        sb.addEvent(product, 'click', that.addToCart);        
    });  
}, 
destroy : function () { 
    var that = this; 
    eachProduct(function (product) { 
        sb.removeEvent(product, 'click', that.addToCart);         
    }); 
    sb.ignore(['change-filter', 'reset-filters', 'perform-search', 'quit-search']); 
},

Inside init, we collect all the products (which are represented in list items). Then, we have to let the sandbox know that we’re interesting in several events. We pass an object to the sb.listen method; this object uses the event name as the key and the event function as a value for each property. For example, we’re telling sandbox that when someone else executes a ‘perform-search’ event, we want to respond to that by executing our search function. Hopefully, you’re starting to see how this will work!

Then, we use our eachProduct helper function to assign an on-click function to each product. When a product it clicked, we run addToCart. We have to cache this because its value changes to the global object inside the function.

In destroy, we simply remove the event handlers from the products and let the sandbox know we’re no longer interested in the events (actually, I don’t think this is necessary, because of the way we handle things in the core, but I threw it in just in case something on the “back end” changes).

Now we’ll look at the functions that get called when other modules fire events:

search : function (query) { 
    reset(); 
    query = query.toLowerCase(); 
    eachProduct(function (product) { 
        if (product.getElementsByTagName('p')[0].innerHTML.toLowerCase().indexOf(query) < 0) { 
            product.style.opacity = '0.2'; 
        }  
    }); 
}, 
change_filter : function (filter) { 
    reset(); 
    eachProduct(function (product) { 
        if (product.getAttribute('data-8088-keyword').toLowerCase().indexOf(filter.toLowerCase()) < 0) { 
            product.style.opacity = '0.2'; 
        } 
    }); 
}, 
addToCart : function (e) { 
    var li = e.currentTarget; 
    sb.notify({ 
        type : 'add-item', 
        data : { id : li.id, name : li.getElementsByTagName('p')[0].innerHTML, price : parseInt(li.id, 10) } 
    });  
}

We’ll start with search; this function is called when the ‘perform-search’ action takes place. As you can see, it takes the search query as a parameter. First, we reset the product area (just in case the results of a previous search or filtering are there). Then, we loop over each product with our helper function. Remember, the product is a list item; inside it is a paragraph with the product description, and that’s what we’ll be searching (in a real world example, this probably wouldn’t be the case). We get the text of the paragraph and compare it to the text of the query (notice that both have been run through toLowerCase()). If the result is less than 0, meaning that no match was found, we’ll set the opacity of the product to 0.2. That’s how we’ll be hiding products in this example. That’s it!

Now’s a good time to point out that all the reset function does is set the opacity of all the products back to 1.

The changeFilter method is pretty similar to search; this time, instead of searching the product description, we take advantage of the HTML5 data-* attributes. These allow us to add custom attributes to our HTML elements without breaking spec rules. However, they must start with ‘data-‘, and I’ve added a personal prefix as well, so they don’t conflict with attributes third party code might use. The filter passed into this function is compared to the data attribute, which will contain the names of the categories of the items. If there are no matches, we’ll reduce the opacity of the element.

The final function is addToCart, which is run when one of the products is clicked. We’ll get the clicked element and then send a notification to the system, informing it about our ‘add-item’ event. This time, the data we’re passing in is an object. It contains the product id, the product name, and the product price. In this example, we’re being lazy and using the element id as the id and price, and the product description as the name.

The Shopping Cart Module

We’ve got one more module to look at. It’s the ‘shopping-cart’ module:

CORE.create_module(“shopping-cart”, function (sb) {  
    var cart, cartItems; 
 
    return { 
        init : function () { 
            cart = sb.find('ul')[0];  
            cartItems = {}; 
 
            sb.listen({ 
                'add-item' : this.addItem 
            }); 
        }, 
        destroy : function () { 
            cart = null; 
            cartItems = null; 
            sb.ignore(['add-item']); 
        }, 
        addItem : function (product) { 
        } 
    }; 
});

I think you’re getting the hang of this now; we’re using two variables: the cart and the items in the cart. On initialization of the module, we’ll set these to the ul in the shopping cart and an empty object respectively. Then, we’ll let the sandbox know that we want to respond to one event. On destruction, we’ll undo all that.

Here’s what should happen when an item is added to the cart:

addItem : function (product) { 
    var entry = sb.find('#cart-' + product.id + ' .quantity')[0]; 
    if (entry) { 
        entry.innerHTML =  (parseInt(entry.innerHTML, 10) + 1); 
        cartItems[product.id]++; 
    } else { 
        entry = sb.create_element('li', { id : "cart-" + product.id, children : [ 
                sb.create_element('span', { 'class' : 'product_name', text : product.name }), 
                sb.create_element('span', { 'class' : 'quantity', text : '1'}), 
                sb.create_element('span', { 'class' : 'price', text : '$' + product.price.toFixed(2) }) 
                ], 
                'class' : 'cart_entry' }); 
 
        cart.appendChild(entry); 
        cartItems[product.id] = 1; 
    } 
 
}

This function takes the product object that we just saw in the product-panel module. Then, we get the element with the selector ‘#cart-’ + product.id + ’ .quantity’; this looks for an element with a class of ‘quantity’ within an element with an id of “cart-id_number”. If this product has been added to the cart before, this will be found. If it is found, we’ll increment the innerHTML of that element (the quantity of that product that the user has added to the car) by one and update the entry in the cartItems object, which keeps track of the purchase.

If the element was not found, this is the first time the user has added one of this product to the cart. In that case, we’ll use the sandbox’s create_element method; as you can see, it will take an attributes object similar to jQuery. The special case here is the children property, which is an array of elements to insert into the element we’re creating. As you can see, we’re basically creating a list item with three spans: the product name, quantity, and price. Then, we append this list item to the cart and add the product to the cartItems object.

That’s all the code for our modules; I should note that I’ve put this all in a file named modules.js. Now that we know what interface our modules will need to work with, we’re ready to build that … and that’s the sandbox.


The Support: Sandbox

I know I’ve mentioned this already, but it’s pretty important that the outward-facing interface of the sandbox does not change. This is because all the modules depend on it. Sure, you can add methods or change the code within methods, so long as you don’t change the methods or what the function does/returns.

If we distill the modules.js file, we’ll see that these are the methods the sandbox needs to give the modules:

  • find
  • addEvent
  • removeEvent
  • notify
  • listen
  • ignore
  • create_element

So let’s get to work. Because I want to create a sandbox instance using the code Sandbox.create, we’ll make it an object with (currently) only one method.

var Sandbox = { 
    create : function (core, module_selector) { 
            var CONTAINER = core.dom.query('#' + module_selector); 
            return { 
                 
            }; 
        }     
};

Here’s our start. As you can see, the create method will take two parameters: a reference to the core and the name of the module it’s going to be given to. Then, we create a variable, CONTAINER, which will reference the DOM element that corresponds with the module code. Now, let’s start coding the functions we listed about.

find : function (selector) { 
    return CONTAINER.query(selector); 
},

This is pretty simple. In fact, most of the functionality in the sandbox is pretty simple, because it’s supposed to be a thin wrapper that gives the module just the right amount of access to the core. When the core.dom.query method we called about returned the container, it gave the container a method that lets it search for child element by selector; we’re using this to limit a module’s ability to affect the DOM, thus keeping it a module in the HTML as well as the JavaScript.

addEvent : function (element, evt, fn) { 
    core.dom.bind(element, evt, fn); 
}, 
removeEvent : function (element, evt, fn) { 
    core.dom.unbind(element, evt, fn); 
},

Like I said, most of these sandbox functions are pretty small; we’ll just shuttle the event data to the core for hook up.

notify : function (evt) { 
    if(core.is_obj(evt) && evt.type) { 
        core.triggerEvent(evt); 
    } 
}, 
listen : function (evts) { 
    if (core.is_obj(evts)) { 
        core.registerEvents(evts, module_selector); 
    }             
},  
ignore : function (evts) { 
    if (core.is_arr(evts)) { 
        core.removeEvents(evts, module_selector); 
    }          
},

These three functions, as you’ll remember, are the vehicles that the modules use to inform other modules about their actions. I’ve added a bit of error checking to these to make sure the event data is all okay before we send it along to the core. Note that when telling the core what we’re listening for or ignoring, we need to pass the name of the module as well.

create_element : function (el, config) { 
    var i, text; 
    el = core.dom.create(el); 
    if (config) { 
        if (config.children && core.is_arr(config.children)) { 
            i = 0; 
            while (config.children[i]) { 
                el.appendChild(config.children[i])); 
                i++; 
            } 
            delete config.children; 
        } else if (config.text) { 
           text = document.createTextNode(config.text); 
           delete config.text; 
           el.appendChild(text); 
        } 
        core.dom.apply_attrs(el, config); 
    } 
    return el; 
}

This is obviously the longest function in the sandbox (and to be honest, the more I think about it, the more I think it should be in the core, but anyway …). As we know, it takes the name of an element and a configuration object. We start by creating the DOM element (use a core method). Then, if there is a config object, and it has an array called children, we’ll loop over each child and append it to the element. Then we delete the children property Otherwise, if we have a text property, we’ll set the text of the element to that and delete the text property (in this example, we can’t have both text and child elements). Finally, we’ll use another core function to apply the remaining attributes and return the element.

And that’s the end of the sandbox. I realize that this might be a rather simplistic sandbox, but it should give you an idea of the way the sandbox work. Also, as you use it, you’ll be able to add other methods when your modules require it.


The Foundation: Base and Core

Now we’re ready to move on to the core. Here’s what we start with:

var CORE = (function () { 
    var moduleData = {}, debug = true; 
 
    return { 
        debug : function (on) { 
            debug = on ? true : false; 
        }, 
 
    }; 
 
}());

We’ll be using the module data object to store everything there is to know about the modules; the debug variable controls whether or not errors are logged to the console. We’ve got a simple debug function to turn errors on and off.

Let’s start with that create_module function that we used to register our modules:

create_module : function (moduleID, creator) { 
    var temp; 
    if (typeof moduleID === 'string' && typeof creator === 'function') { 
        temp = creator(Sandbox.create(this, moduleID)); 
        if (temp.init && temp.destroy && typeof temp.init === 'function' && typeof temp.destroy === 'function') { 
            moduleData[moduleID] = { 
                create : creator, 
                instance : null 
            }; 
            temp = null; 
        } else { 
            this.log(1, "Module \"" + moduleId + "\" Registration: FAILED: instance has no init or destroy functions"); 
        } 
    } else { 
        this.log(1, "Module \"" + moduleId +  "\" Registration: FAILED: one or more arguments are of incorrect type" ); 
 
    } 
},

The first thing we do is confirm that the parameters passed to the function were of the right type; if they aren’t we’ll call a log function, which takes a severity number and a message (we’ll see the log function is a few minutes).

Next, we create a copy of the module in question just to make sure it has the init and destroy functions; if it doesn’t, we again log an error. If all checks out, however, we’ll add an object to moduleData; we’re storing the creator function and an empty spot for the instance when we start the module. Then we’ll delete the temporary copy of the module.

start : function (moduleID) { 
    var mod = moduleData[moduleID]; 
    if (mod) { 
        mod.instance = mod.create(Sandbox.create(this, moduleID)); 
        mod.instance.init(); 
    } 
}, 
start_all : function () { 
    var moduleID; 
    for (moduleID in moduleData) { 
        if (moduleData.hasOwnProperty(moduleID)) { 
            this.start(moduleID); 
        } 
    } 
},

Next, we’ll add the function for starting the modules up; as you might expect, it accepts a module name as the single parameter. If there’s a corresponding module in moduleData, we’ll run its create method, passing it a new sandbox instance. Then, we’ll start it up by running its init method.

We can create a function that makes it easy to start all the modules at once, since that’s probably something we’ll want to do. We just have to loop over moduleData and send each moduleID to the start method. Don’t forget to use the hasOwnProperty part; I know it seems unnecessary and ugly (at least to me), but it’s there just in case someone has added item to the object’s prototype object.

stop : function (moduleID) { 
	var data; 
	if (data = moduleData[moduleId] && data.instance) { 
		data.instance.destroy(); 
		data.instance = null; 
	} else { 
		this.log(1, "Stop Module '" + moduleID + "': FAILED : module does not exist or has not been started"); 
	} 
}, 
stop_all : function () { 
	var moduleID; 
	for (moduleID in moduleData) { 
		if (moduleData.hasOwnProperty(moduleID)) { 
			this.stop(moduleID); 
		} 
	} 
},

The next two functions should be obvious: stop and stop_all. The stop function takes a module name; if the system knows about a module with that name and that module is running, we’ll call that module’s destroy method, and then set the instance to null. If the module doesn’t exist or isn’t running, we’ll log the error.

The stop_all function is exactly like start_all, except is calls stop on each module.

Next up: dealing with the events.

registerEvents : function (evts, mod) { 
	if (this.is_obj(evts) && mod) { 
		if (moduleData[mod]) { 
			moduleData[mod].events = evts; 
		} else { 
			this.log(1, ""); 
		} 
	} else { 
		this.log(1, ""); 
	} 
}, 
triggerEvent : function (evt) { 
	var mod; 
	for (mod in moduleData) { 
		if (moduleData.hasOwnProperty(mod)){ 
			mod = moduleData[mod]; 
			if (mod.events && mod.events[evt.type]) { 
				mod.events[evt.type](evt.data); 
			} 
		} 
	} 
}, 
removeEvents : function (evts, mod) { 
	var i = 0, evt; 
	if (this.is_arr(evts) && mod && (mod = moduleData[mod]) && mod.events) { 
		for ( ; evt = evts[i++] ; ) { 
				delete mod.events[evt]; 
			} 
	} 
},

As we know, registerEvents takes an object of events and the module that’s registering them. Again, we do some error checking (I’ve left the errors blank in this case, just to simplify the example). If evts is an object and we know which module we’re talking about, we’ll just stuff the object into the module’s locker in moduleData.

When it comes to triggering events, we’re given an object with a type and data. We’ll loop over each module in moduleData once again: if the module has an event property and that event object has a key corresponding to the event we’re executing, we’ll call the function stored for that event and pass it the event’s data.

Removing events is even simpler; we get the events object and (after the usual error checking) we loop over it and remove the events in the array from the module’s event object. (Note: I think I got this a bit mixed up on the screencast, but this is the correct version.)

log : function (severity, message) { 
	if (debug) { 
		console[ (severity === 1) ? 'log' : (severity === 2) ? 'warn' : 'error'](message); 
	} else { 
		// send to the server 
	}      
},

Here’s that log function that’s been haunting us for a while now; basically, if we’re in debug mode, we’ll log errors to the console; otherwise we’ll send them to the server. Oh, the fancy ternary stuff? That just uses the severity argument to decide which of Firebug’s functions to use to log the error: 1 === console.log, 2 === console.warn, >2 === console.error.

Now we’re ready to look at the part of the core that gives the sandbox the base functionality; for most of this, I’ve sub-classed them in a dom object, because I’m obsessive compulsive that way.

dom : { 
	query : function (selector, context) { 
		var ret = {}, that = this, jqEls, i = 0; 
 
		if (context && context.find) { 
			jqEls = context.find(selector); 
		} else { 
			jqEls = jQuery(selector); 
		} 
		 
		ret = jqEls.get(); 
		ret.length = jqEls.length; 
		ret.query = function (sel) { 
			return that.query(sel, jqEls); 
		} 
		return ret; 
	},

Here’s our first function, query. It takes a selector and a context. Now remember, this is the core, where we can directly reference the base (which is jQuery). In this case, the context should be a jQuery object. If the context has a find method, we’ll set jqEls to the result of context.find(selector); if you’re familiar with jQuery, you’ll know that this will only get the element that are children of context; that’s how we get the functionality of sandbox.query! Then, we’ll set our return object to the result of calling jQuery’s get method; this returns an object of raw dom elements. Then, we give ret the length property, so it can be looped over easily. Finally, we give it a query function: this function takes only one parameter, a selector, and it calls core.dom.query, passing that selector and jqEls as the parameters. That’s it!

bind : function (element, evt, fn) { 
	if (element && evt) { 
		if (typeof evt === 'function') { 
			fn = evt; 
			evt = 'click'; 
		} 
		jQuery(element).bind(evt, fn); 
	} else { 
		// log wrong arguments 
	} 
}, 
unbind : function (element, evt, fn) { 
	if (element && evt) { 
		if (typeof evt === 'function') { 
			fn = evt; 
			evt = 'click'; 
		} 
		jQuery(element).unbind(evt, fn); 
	} else { 
		// log wrong arguments 
	} 
},

In the DOM event bind and unbind functions, I’ve decided to provide a perk for the user. The users must pass at lease two functions, but if the evt parameter is a function, then we’ll assume the user has left our the event type they want to handle. In that case, we’ll assume a click, since it’s the most common. Then, we just use jQuery’s bind function to wire it up. I should note that because our query function returns (minimally) wrapped DOM sets, similar to jQuery’s, this function can pass our set to the Jquery object and it has no problems with it.

Our unbind function is exactly the same, except, of course, that it unbinds the events.

    create: function (el) { 
        return document.createElement(el);         
    }, 
    apply_attrs: function (el, attrs) { 
        jQuery(el).attr(attrs);              
    } 
}, // end of dom object 
is_arr : function (arr) { 
    return jQuery.isArray(arr);          
}, 
is_obj : function (obj) { 
    return jQuery.isPlainObject(obj);          
}

I’ve grouped the last four functions together because they’re all pretty simple; dom.create simply returns a new DOM element; dom.apply_attrs uses jQuery’s attr method to give the element attributes. Finally, we’ve got the two helper functions that we’ve been using to verify our parameters.

Believe it or not, that’s the whole core; and our base is jQuery, so we’re ready to assemble this.


Putting It All Together

Now we’re ready to build some HTML so we can see our JavaScript in action. I won’t walk you through this in detail, because that’s not the point of the tutorial.

<!DOCTYPE HTML> 
<html lang="en"> 
<head> 
    <meta charset="UTF-8"> 
    <title>Online Store</title> 
    <link rel="stylesheet" href="default.css" /> 
</head> 
<body> 
    <div id="main"> 
        <div id="search-box"> 
            <input id="search_input" type="text" name='q' /> 
            <button id="search_button">Search</button> 
            <button id="quit_search">Reset</button> 
        </div> 
 
        <div id="filters-bar"> 
            <ul> 
                <li><a href="#red">Red</a></li> 
                <li><a href="#blue">Blue</a></li> 
                <li><a href="#mobile">Mobile</a></li> 
                <li><a href="#accessory">Accessory</a></li> 
            </ul> 
        </div> 
 
       <div id="product-panel"> 
            <ul> 
                <li id="1" data-8088-keyword="red"><img src="img/1.jpg"><p>First Item</p></li> 
                <li id="2" data-8088-keyword="blue"><img src="img/2.jpg"><p>Second Item</p></li> 
                <li id="3" data-8088-keyword="mobile"><img src="img/3.jpg"><p>Third Item</p></li> 
                <li id="4" data-8088-keyword="accessory"><img src="img/4.jpg"><p>Fourth Item</p></li> 
                <li id="5" data-8088-keyword="red mobile"><img src="img/5.jpg"><p>Fifth Item</p></li> 
                <li id="6" data-8088-keyword="blue mobile"><img src="img/6.jpg"><p>Sixth Item</p></li> 
                <li id="7" data-8088-keyword="red accessory"><img src="img/7.jpg"><p>Seventh Item </p></li> 
                <li id="8" data-8088-keyword="blue accessory"><img src="img/8.jpg"><p>Eighth Item</p></li> 
                <li id="9" data-8088-keyword="red blue"><img src="img/9.jpg"><p>Ninth Item</p></li> 
                <li id="10" data-8088-keyword="mobile accessory"><img src="img/10.jpg"><p>Tenth Item</p></li> 
            </ul> 
 
        </div> 
         
        <div id="shopping-cart"> 
            <ul> 
            </ul>  
        </div>  
    </div> 
    <script src="js/jquery.js"></script> 
    <script src="js/core-jquery.js"></script> 
    <script src="js/sandbox.js"></script> 
    <script src="js/modules.js"></script> 
</body> 
</html>

It’s nothing fancy; the important thing to notice is that each of the main divs have ids that corresponds to the JavaScript modules. And don’t forget about the HTML5 data-* attributes that give us the categories to filter through.

Of course, we’ll need to style it:

 
body { 
    background:#ececec; 
    font:13px/1.5 helvetica, arial, san-serif; 
} 
#main {  
    width:950px; 
    margin:auto;  
    overflow:hidden;  
} 
#search-box, #filters-bar {  
    margin-left:10px;  
} 
#filters-bar ul {  
    list-style-type:none;  
    margin:10px 0;  
    padding:0;  
    border-top:2px solid #474747;  
    border-bottom:2px solid #474747;  
    } 
#filters-bar li {  
    display:inline-block;  
    padding:5px 10px 5px 0; 
} 
#filters-bar li a { 
    text-decoration:none; 
    font-weight:bold; 
    color:#474747; 
} 
#product-panel { 
    float:left; 
    width: 588px; 
} 
#product-panel ul { 
    margin:0; 
    padding:0; 
} 
#product-panel li { 
    list-style-type:none; 
    display:inline-block; 
    text-align:center; 
    background:#474747; 
    border:1px solid #eee; 
    padding:15px; 
    margin:10px; 
} 
#product-panel li p { 
    margin:10px 0 0 0; 
} 
 
#shopping-cart { 
    float:left; 
    background:#ccc; 
    height:300px; 
    width:300px; 
    padding:30px; 
    border:1px solid #474747; 
} 
 
#shopping-cart ul { 
    list-style-type:none; 
    padding:0; 
} 
 
#shopping-cart li { 
    padding:3px; 
    margin:2px 0; 
    background:#ececec; 
    border: 1px solid #333; 
} 
 
#shopping-cart .product_name { 
    display:inline-block; 
    width:230px; 
} 
 
#shopping-cart .price { 
    display: inline-block; 
    float:right; 
}

It’s hard to show it in action, (that’s what the screencast is for!), but here are a few shot:

Well, that would be all, but let’s do one more thing: let’s create a core that works on Dojo, to show how using a core as we did makes it easy to switch bases.

Why Dojo? To be completely honest, I gave Mootools and YUI a try, but there were a few challenges that were going to take longer than the time I had to figure it out. I certainly don’t think it’s impossible to use them; if you build a core with Mootools, YUI, or some other JavaScript framework, I’d love to see it. For now, let’s build one with Dojo.

Of course, we don’t have to change any of the module-handling functions; in our case, all we need to change is the dom section, is_arr, and is_obj.

query : function (selector, context) { 
    var ret = {}, that = this, len, i =0, djEls; 
 
    djEls = dojo.query( ((context) ? context + " " : "") + selector); 
 
    len = djEls.length; 
 
    while ( i < len) { 
        ret[i] = djEls[i++]; 
    } 
    ret.length = len; 
    ret.query = function (sel) { 
        return that.query(sel, selector); 
    } 
    return ret; 
},

Here’s the query function, rewritten to work with Dojo. As you can see, it does exactly what the jQuery version does; the main difference is that context is a string that’s appended to the front of the selector.

Now for the DOM events functions:

eventStore : {}, 
bind : function (element, evt, fn) { 
    if (element && evt) { 
        if (typeof evt === 'function') { 
            fn = evt; 
            evt = 'click'; 
        } 
        if (element.length) { 
            var i = 0, len = element.length; 
            for ( ; i < len ; ) { 
                this.eventStore[element[i] + evt + fn] = dojo.connect(element[i], evt, element[i], fn); 
                i++; 
            } 
        } else { 
           this.eventStore[element + evt + fn] = dojo.connect(element, evt, element, fn); 
        } 
    } 
}, 
unbind : function (element, evt, fn) { 
    if (element && evt) { 
        if (typeof evt === 'function') { 
            fn = evt; 
            evt = 'click'; 
        } 
         if (element.length) { 
            var i = 0, len = element.length; 
            for ( ; i < len ; ) { 
                dojo.disconnect(this.eventStore[element[i] + evt + fn]); 
                delete this.eventStore[element[i] + evt + fn]; 
                i++; 
            } 
        } else { 
            dojo.disconnect(this.eventStore[element + evt + fn]); 
            delete this.eventStore[element + evt + fn]; 
        } 
    } 
},

You’ll notice we’ve added an eventStore object; this is because Dojo binds events with dojo.connect and unbinds with dojo.disconnect; the catch here is that dojo.disconnect takes the object returned from dojo.connect as its only parameter. We use eventStore to keep track of those values so we can easily disconnect events. The rest of the complexity here is just looping over our wrapped DOM sets, because Dojo can’t handle those alone.

    create: function (el) { 
        return document.createElement(el);         
    }, 
    apply_attrs: function (el, attrs) { 
        var attr; 
        for (attr in attrs) { 
            dojo.attr(el, attr, attrs[attr]); 
        } 
    } 
}, // end of dom object 
is_arr : function (arr) { 
    return dojo.isArray(arr); 
}, 
is_obj : function (obj) { 
    return dojo.isObject(obj); 
}

This part shouldn’t be too hard to understand; it’s all pretty similar to jQuery’s.

Now, you should be able to include Dojo as your base and our new Dojo core as your core and our app will continue to function as before.


Conclusion: Should You Code This Way?

Well, now that we’ve seen exactly how a system like this should work, let’s talk about whether you’d want to write your JavaScript apps like this. Obviously, this isn’t a pattern you’d use on your average website; it’s for full-fledged applications. Here’s what I came up with:

Cons:

  • It can be pretty difficult to learn this type of coding. Thinking modularly definitely takes a paradigm shift.
  • When you’re starting out, it’s hard to know how much power to give the layers. What should modules be able to do? Should there be any real functionality in the sandbox, or is it just there to expose the right amount of core functionality to the modules? Where do we do error checking? Can we perform simply tasks, like creating dom elements, within the modules? I’m sure you’ve got your own perspectives on there and more questions, so let me know what you think, in either the comments on the Nettuts+ post or via the form on my website. Or, even better: write about it on your blog / website for the world to see, and be sure to send me a link.
  • Finally, it’s weird to use a JavaScript framework as the base; with my current understanding of this, they don’t really lend themselves to doing this.

Pros

  • Once you’ve got a solid core and sandbox, creating new web apps is going to be so much faster; it’s basically choosing modules from your ever growing module library and hooking them together.
  • It’s a lot easier to test your code, because it’s all so modular.

Well, that’s all I’ve got for you today; I hope I’ve stretched your mind a bit; I know learning about all this in the last week or so has stretched mine! I’d love to know what you think about it all, so please let me know if you have comments! Thanks for reading!

Advertisement