I am trying to build my own little jquery-like library but I'm having a really rough time with creating this chaining pattern. Basically I have one class with a bunch of methods that make it easier to manipulate the document. Here's an example
function MF(selector){
var DO; // Stands for DocumentObject
this.select = function(selector){
return document.getElementById(selector);
}
if(typeof selector === 'string'){
DO = this.select(selector);
}else if(selector instanceof HTMLElement){
DO = selector;
}
this.children = function children(selector){
return DO.getElementsByClassName(selector);
}
return {
MF: ???
}
}(null);
I might be wrong in my reflections but what I've e to figure out is that in order to have additional methods for a document object ( html element ) I either need to extend the HTMLElement prototype or pass the element along with my class. I've chosen the second option. I just can't figure out what to return in my class so that I can have chaining going on. What I simply aim at, for the sake of this example, is to be able to write the following line of code:
MF('someDiv').children('someClass');
In a desperate attempt I tried returning a new instance of MF
which should not have instances by default and led myself to an infinite loop. I really cannot figure what I'm supposed to return there. Any help is greatly appreciated!
I am trying to build my own little jquery-like library but I'm having a really rough time with creating this chaining pattern. Basically I have one class with a bunch of methods that make it easier to manipulate the document. Here's an example
function MF(selector){
var DO; // Stands for DocumentObject
this.select = function(selector){
return document.getElementById(selector);
}
if(typeof selector === 'string'){
DO = this.select(selector);
}else if(selector instanceof HTMLElement){
DO = selector;
}
this.children = function children(selector){
return DO.getElementsByClassName(selector);
}
return {
MF: ???
}
}(null);
I might be wrong in my reflections but what I've e to figure out is that in order to have additional methods for a document object ( html element ) I either need to extend the HTMLElement prototype or pass the element along with my class. I've chosen the second option. I just can't figure out what to return in my class so that I can have chaining going on. What I simply aim at, for the sake of this example, is to be able to write the following line of code:
MF('someDiv').children('someClass');
In a desperate attempt I tried returning a new instance of MF
which should not have instances by default and led myself to an infinite loop. I really cannot figure what I'm supposed to return there. Any help is greatly appreciated!
-
Hint:
MF('someDiv')
should return an object and methods which don't return anything else (unlike.children
) should returnthis
. – Aadit M Shah Commented Nov 2, 2013 at 23:20
4 Answers
Reset to default 2It looks like you also tried to use the module pattern, and haven't taken advantage of prototypes..
When you want to chain something, you either need to return itself (this
) within the chainable methods, or return a new
instance of your Object. In the example below I achieve chaining by using new instances (well the children looked like it needed to be a list so I did an Array of them).
var MF = (function () { // module pattern start
function MF(selector) {
if (!(this instanceof MF)) return new MF(selector); // always construct
this.node = null; // expose your DO
if (typeof selector === 'string') {
this.node = document.getElementById(selector);
} else if (selector instanceof HTMLElement) {
this.node = selector;
} else {
throw new TypeError('Illegal invocation');
}
}
MF.prototype = {}; // set up any inheritance
MF.prototype.select = function (selector) {
return new MF(document.getElementById(selector)); // returns new instance
};
MF.prototype.children = function (selector) {
var MFs = [],
nodes = this.node.getElementsByClassName(selector),
i;
for (i = 0; i < nodes.length; ++i) {
MFs[i] = new MF(nodes[i]);
}
return MFs; // array of items of new instances
};
return MF; // pass refence out
}()); // module pattern end
Then, for example, you can chain like..
MF(document.body).children('answer')[0].children('post-text')[0].node;
return this;
will allow access to methods of the Constructor. Do it at the very bottom of the Constructor and the very bottom inside every method that belongs to it, if the method doesn't need to return another value.
function MF(selector){
var doc = document;
this.select = function(selector){
return doc.getElementById(selector);
}
// there are problems with some of your code
this.someMethod = function(){
/* do stuff - If you want to access an Element then
var thisIsNowGlobal = this.select('someId');
thisIsNowGlobal.innerHTML = 'someText';
Note, the keyword this is global not var
If you wrote this.select('someId').innerHTML the property would not exist
When a property of an Object is assigned to a variable or argument
the this value changes to the global context.
*/
return this;
}
return this;
}
You can do this pretty easily. Bear in mind that when you return this
then if this
has methods defined on it, then you can call them sequentially.
var MyUtilThing = function(){};
MyUtilThing.prototype.doStuff = function doStuff (){ // give it a name, helps in debugging
// do your thing
console.log('doing stuff');
return this; // this is your instance of MyUtilThing
}
var thing = new MyUtilThing();
thing.doStuff().doStuff().doStuff(); // etc
One way around having to explicitly create instances is do this in your constructor.
var MyUtilThing = function(selector){
var F = function(){};
F.prototype = MyUtilThing.prototype;
var toReturn = new F();
toReturn.initialize(selector);
return toReturn;
};
MyUtilThing.prototype.initialize = function initialize(selector){
this.selector = selector;
};
MyUtilThing.prototype.doStuff = function doStuff (){ // give it a name, helps in debugging
// do your thing
console.log('doing stuff to', this.selector);
return this; // this is your instance created in the constructor (the blank function with the same prototype as MyUtilThing)
}
var thing = MyUtilThing('div'); // no use of new here!
thing.doStuff().doStuff().doStuff(); // etc
But, getting into some slightly heavy territory there. Best bet is just to try and understand exactly how this
is used in JS, and you'll get a long way.
Traditionally the way jQuery enables chaining is by creating a wrapper object for every type of return value. For example in your case it pays to create your own wrapper for HTMLElement
:
function HTMLElementWrapper(element) {
if (element instanceof HTMLElementWrapper) return element;
this.element = element;
}
Now that you have an HTMLElementWrapper
you can refactor your MF
function as follows:
function MF(selector) {
return new HTMLElementWrapper(typeof selector === "string" ?
document.getElementById(selector) : selector);
}
The MF
function now returns an HTMLElementWrapper
object which has two methods select
and children
:
HTMLElementWrapper.prototype.select = function (selector) {
return new HTMLElementWrapper(this.element.getElementById(selector));
};
HTMLElementWrapper.prototype.children = function (selector) {
return new NodeListWrapper(this.element.getElementsByClassName(selector));
};
Ofcourse for the children
function to work you'll need to create a NodeListWrapper
constructor:
function NodeListWrapper(list) {
if (list instanceof NodeListWrapper) return list;
this.list = list;
}
Now you can chain methods as follows:
MF("someDiv").select("something").children("someClass");
To chain methods after .children("someClass")
you need to add those methods to NodeListWrapper.prototype
.