Chaining requires two parts:
- a factory (factory pattern) that creates an object around an HTML element,
- and methods that perform some action using that HTML element.
Strucure of a Chain
Here is the dollar function, which usually returns an HTML element or a collection of HTML elements as shown here:
function $() {
var elements = []; for (var i = 0, len = arguments.length; i < len; ++i) {
}
var element = arguments[i]; if (typeof element === 'string') {
element = document.getElementById(element);
} if (arguments.length === 1) {
return element; elements.push(element);
}
} return elements;
Basically, you modify function to act as a constructor, store the elements as an array in an instance property, then return a reference to the instance in all prototype methods, you can give it the ability to chain.
Building it:
You modify the dollar function so it becomes a factory method, creating an object that will support chaining.
Here is modified code:
(function() {
// Use a private class.
function _$(els) {
this.elements = [];
for (var i = 0, len = els.length; i < len; ++i) {
var element = els[i];
if(typeof element === 'string') {
element = document.getElementById(element);
}
this.elements.push(element);
}
}
// The public interface remains the same.
window.$ = function() {
return new _$(arguments);
};
})();
Since all objects inherit from their prototype, you can take advantage of the reference to the instance object being returned and run each of the methods attached to the prototype as a chain.
Here is the code:
(function() {
// Use a private class.
function _$(els) {
this.elements = [];
for (var i = 0, len = els.length; i < len; ++i) {
var element = els[i];
if(typeof element === 'string') {
element = document.getElementById(element);
}
this.elements.push(element);
}
}
//adding methods to private dollar constructor:
_$.prototype = {
each: function(fn){
for(var i = 0, len = this.elements.length; i < len; ++i) {
fn.call(this, this.elements[i]);
}
return this;
},
setStyle: function(prop, val) {
this.each(function(){
el.style[prop] = val;
});
return this;
},
show: function() {
var that = this;
this.each(function(el) {
that.setStyle('display', 'block');
});
return this;
},
addEvent: function(type, fn) {
var add = function(el) {
if(window.addEventListener) {
el.addEventListener(type, fn, false);
}else if(window.attachEvent) {
el.attachEvent('on' + type, fn);
}
};
this.each(function(el){
add(el);
});
return this;
}
};
// The public interface remains the same.
window.$ = function() {
return new _$(arguments);
};
})();
Now you can write code like this:
$(window).addEvent('load', function() {
$('test-1', 'test-2').show().
setStyle('color', 'red').
addEvent('click', function(e) {
$(this).setStyle('color', 'green');
});
});
Using Callbacks to Retrieve Data from Chained Methods
For mutator methods, they're just fine, but with accessor methods, you may wish to return the data that you are requesting, instead of returning this. You can work around this problem by using function callbacks to return your accessed data. Here is simple example://Accessor with function callbacks.
window.API2 = window.API2 || {};
API2.prototype = (function() {
var name = 'Hello world';
// Privileged mutator method.
setName: function(newName) {
name = newName;
return this;
},
// Privileged accessor method.
getName: function(callback) {
callback.call(this, name);
return this;
}
})();
//Implementation code.
var o2 = new API2;
o2.getName(console.log).setName('Meow').getName(console.log);
//Displays 'Hello world' and then 'Meow'.
Summary
JavaScript passes all objects by reference, so you can pass these references back in every method. By returning this at the end of each method, you can create a class that is chainable.This style helps to streamline code authoring and to a certain degree, make your code more elegant and easier to read.
Often you can avoid situations where objects are declared several times and instead use a chain.
If you want consistent interfaces for your classes, and you want both mutator and accessor methods to be chainable, you can use function callbacks for your accessors.