Earlier this year, I ranted about how the graceful degradation model of jQuery made it hard to create complex components. Also, while working with a team on JavaScript components, I had to review all my previous takes on JavaScript architecture in order to build conventions that an entire team can follow.
Namespacing
To avoid collisions with other libraries, I create an object that uses a name I own. Any namespace name strategy is possible here, from java-like netNicolletCheese (if you have a common project name) to just cheese (if you have a project name that’s fairly unique). Then, any code I write goes into that namespace. I may further add sub-namespaces if I have a lot of code. Either way, you have to make sure the namespace exists before adding things to it, thus I add at the top of every file:
if(!('netNicolletCheese' in this)) this.netNicolletCheese = {};
The basic idea is that since I don’t know what order my files will be defined in, I have to define the namespace in every single one, while avoiding redefinition. This way, I can include files on an if-needed basis or stick them all together and remove any occurences of the namespace line except the first.
Then, everything is defined as members of that object. Executing any kind of code in library files is forbidden, only function and object definitions are allowed.
Components
A component is a class that contains data and renders itself somewhere on the page. This is different from the jQuery model of graceful degradation that assumes the rendered data is already present on the page and merely changes its layout. Use with caution, since this loses many benefits of graceful degradation like accessibility or search engine friendliness.
A component is always created as follows:
instance = new namespace.component(selector, data, options);
- It’s always assigned to a variable. It’s a global variable if it’s defined at global scope (obviously, this may only happen in the code on a page, not in library code), and a public member variable of another object if it’s defined within an object. There are no free-floating components, every single one must be accessible from global scope as this makes command-line debugging way easier, and keeps the structure easier to see.
- It has a first argument, which is a selector (in the jQuery sense). It will be fed to $(…) in order to get the target elements of the component (usually a single one). The typical behavior of a component is to generate some HTML from its internal state and call $(selector).html(…) to display the HTML. The selector is evaluated when the constructor is called, which means you may have to wrap the object initialization in a $(document).ready(…) to wait for the DOM to be instantiated. It also means adding any elements matching the selector later on won’t have any effect on the component.
- It has a second argument, which is the data used to initialized the component. For instance, if the component is intended to display a list of elements, the data argument would be that list en JSON notation. This makes it easy to generate that data on the server side using one of the many JSON generators, while also making the component easy to instance on the client side programmatically.
- It has an optional third argument, which represents the options that one may provide the component with (such as width, height, speed, effects, and so on). If it’s not part of the main data argument, it’s part of the options. The options are a classic JS record.
Component Initialization
The component is instantiated either when the document is ready, by placing the initialization code in the appropriate event, such as :
var page = {};
$(function(){ page.instance = new namespace.component(selector, data, options) });
Or it can be instantiated inside another component an an appropriate time.
The constructor itself consists of two distinct operations :
- Set up any member variables representing the internal object state, using the data argument and options argument.
- Render the object so that it appears on the page, using the rendering function, and passing the selector to it:
this.render(selector);
Note that a component may be created without a target selector, simply by using an empty array as the selector. It will remain unrendered until its render function is manually called with a valid selector as its argument.
Component Rendering
The render function is called during initialization. It’s also called whenever the entire component needs to be redrawn. Some components are small, and are redrawn every time, while other components may choose to only redraw parts of their contents and may therefore use other rendering functions for those parts. The rendering function reliably performs up to six operations:
- It initializes the target, if it was provided. This lets the calling code change the rendering target dynamically.
if (typeof(selector) != "undefined") this.$target = $(selector);
This is generally useful when a component contains other components : a full rendering of the container means the target DOM elements of the inner components have been destroyed and created anew, and the container must therefore notify the inner components that they have a new target to render to.
Note that the name of the target is always the same: for any component,
component.$targetis the current target of the component. - It optionally determines whether there is a target to begin with, to avoid unnecessary work. This usually takes the form :
if (this.$target.get().length == 0) return;
In the case where a component is inside a container, the container will create the component before rendering itself (to make things simpler, rendering assumes all sub-components already exist), and therefore provide an empty array as the selector.
- It generates the full HTML for the component as a string.
- It inserts the HTML into the DOM, replacing anything that previously existed. This usually happens as:
this.$target.html(theGeneratedHtml);
- It changes the rendering target of any sub-components and tells them to render themselves, usually written by extracting the correct targets from its own target and reverting it to an array of DOM elements:
this.subComponent.render(this.$target.find('.subComponent').get()); - It sets up any relevant events on the generated DOM. For instance, if the generated HTML contains a button, the button’s click event may be set to an event handler:
this.$target.find('button').click(this.onButtonClick)
Component Event Handlers
It would be easy to define the “on button click” event simply as follows:
namespace.component.prototype.onButtonClick = function()
{ this.data.frobnicate(); }
But that wouldn’t work with jQuery, since the events re-bind the ‘this’ variable on the event handler before calling it. Meaning ‘this’ would be, in this case, the button DOM element instead of our component. This is bad.
The solution is to create an anonymous function that forwards the call to the appropriate member function:
this.$target.find('button').click(function(){this.onButtonClick()})
Whoops. ‘this’ doesn’t follow lexical scoping, which means this code still has the same problem. However, this can be solved quite easily:
var self = this;
this.$target.find('button').click(function(){self.onButtonClick()})
A short example
We can write a short incrementer: a button with a number that increases every time the button is pressed.
// Create the namespace if it doesn't exist if (!('netNicollet' in this)) this.netNicollet = {}; // The constructor for our component netNicollet.counter = function(selector, initial) { // Set up data members (only one) this.value = initial; // Render the component this.render(selector); } // The rendering function netNicollet.counter.prototype.render = function(selector) { // Change the target (if applicable) if (typeof selector != "undefined") this.$target = $(selector); // Early-out if no target if (this.$target.get().length == 0) return; // Generate the HTML var html = '<div>' + this.value + '</div>' + '<button type="button">Increment</button>'; // Insert the HTML into the DOM this.$target.html(html); // Set up the events var self = this; this.$target.find('button') .click(function(){self.increment()}); } // The increment operation netNicollet.counter.prototype.increment = function() { // Change the state this.value++; // Update the graphics this.render(); } // Call this once the document is ready. var counter = new netNicollet.counter('body', 1337);
Hi. I'm Victor Nicollet,
Recent Comments