How to write a jQuery like library in 71 lines of code — Learn about the DOM — Free Code Camp — Medium
Save article ToRead Archive Delete · Log in Log out
9 min read · View original · medium.freecodecamp.com
How to write a jQuery like library in 71 lines of code — Learn about the DOM
JavaScript frameworks are all the rage. Chances are that any JavaScript related news feed you open will be littered with references to tools like ReactJS, AngularJS, Meteor, RiotJS, BackboneJS, jQuery, and beyond.
Anyone learning to code (and even experienced developers) will feel an enormous pressure to learn these new tools. Hype creates demand. If you’re not up to date with what’s in demand, it can feel like your services are not in demand.
I’ve noticed a trend where people are diving headfirst into learning these tools without actually knowing what they do, let alone how they do it. This ultimately makes debugging and conceptualizing the said tool exceptionally hard. There are thousands of cases of misuse where entire projects are being created simply for two way data binding, or for an animation effect, or even just to display an image slider.
Developers are neglecting learning the DOM itself
The DOM, or Document Object Model, is the heart and soul of you web browser . You’re looking at it right now. To clarify, the DOM is not a feature of JavaScript — its not even written in JavaScript — it is a programming interface, a API between language and browser. Language controls calculations etc, browser controls display and events.
I am going to demonstrate below how to create a simple jQuery-like DOM manipulation library. This will be able to target elements using the famous $ selector, create new elements, add html, and control event binding.
Getting Started
We need to create our base object. Let’s call it domElement. This object will act as a wrapper for elements being targeted.
var domElement = function(selector) {
this.selector = selector || null; //The selector being targeted
this.element = null; //The actual DOM element
};
Now we can start adding functionality.
The jQuery methods we will replicate are the selector/creator $() .on(), .off(), .val(), .append, .prepend() and .html()
Lets dive into event binding first. This is by far the most complicated method we will create as well as the most useful. This is the glue in a two-way data binding model. (The model updates its subscribers when an event such as update is triggered, and the subscribers do likewise.)
We will be using a Publish/Subscribe design pattern.
When .on(event, callback) is called we subscribe to the event and similarly when .off(event) is called we unsubscribe from the event.
The event handler will be its own object.
Let’s start by creating a base object and extending the prototype of domElement with it.
domElement.prototype.eventHandler = {
events: [] //Array of events & callbacks the element is subscribed to.
}
Great, now let’s create our subscriber method. We’ll call it bindEvent since it is binding an event listener to our DOM element.
domElement.prototype.eventHandler = {
events: [], //Array of events the element is subscribed to.
bindEvent: function(event, callback, targetElement) {
//remove any duplicate event
this.unbindEvent(event,targetElement);
//bind event listener to DOM element
targetElement.addEventListener(event, callback, false);
this.events.push({
type: event,
event: callback,
target: targetElement
}); //push the new event into our events array.
}
}
That’s it! Lets break the function down quickly
- We remove any existing events on the element that have the the type that is being bound. This is purely a matter of personal preference. I prefer to keep singular event handlers, since they’re easier to manage and debug. Removing the line will allow multiple handlers of the same type. We will create the unbindEvent function a little later.
- We bind the event to the DOM Element, making it live.
- We push the event and all its info into the events array so the element can keep track of our listeners.
Now, before we can remove an event, we will need a method to find and return it from the events array, if it exists. Lets create a quick method to find and return an event by its type, using the built in array filter method.
domElement.prototype.eventHandler = {
events: [], //Array of events the element is subscribed to.
bindEvent: function(event, callback, targetElement) {
//remove any duplicate event
this.unbindEvent(event,targetElement);
//bind event listener to DOM element
targetElement.addEventListener(event, callback, false);
this.events.push({
type: event,
event: callback,
target: targetElement
}); //push the new event into our events array.
},
findEvent: function(event) {
return this.events.filter(function(evt) {
return (evt.type === event); //if event type is a match return
}, event)[0];
}
}
Now we can add our unbindEvent method.
domElement.prototype.eventHandler = {
events: [], //Array of events the element is subscribed to.
bindEvent: function(event, callback, targetElement) {
//remove any duplicate event
this.unbindEvent(event,targetElement);
//bind event listener to DOM element
targetElement.addEventListener(event, callback, false);
this.events.push({
type: event,
event: callback,
target: targetElement
}); //push the new event into our events array.
},
findEvent: function(event) {
return this.events.filter(function(evt) {
return (evt.type === event); //if event type is a match return
}, event)[0];
},
unbindEvent: function(event, targetElement) {
//search events
var foundEvent = this.findEvent(event);
//remove event listener if found
if (foundEvent !== undefined) {
targetElement.removeEventListener(event, foundEvent.event, false);
}
//update the events array
this.events = this.events.filter(function(evt) {
return (evt.type !== event);
}, event);
}
};
And that’s our event handler! Try it out below…
Now that’s quite a useful little utility, but your probably wondering what this has to do with jQuery, and why the methods for the event handler aren't named “on” and “off.”
This is what we will do next. Since we require the event handler to be an object, and we don’t want to call $(‘element’).eventHandler.on(..) our methods will simply point to the correct functions.
Here’s the code for the on and off methods:
domElement.prototype.on = function(event, callback) {
this.eventHandler.bindEvent(event, callback, this.element);
}
domElement.prototype.off = function(event) {
this.eventHandler.unbindEvent(event, this.element);
}
See how that works? Now lets add in our other utility functions…
domElement.prototype.val = function(newVal) {
return (newVal !== undefined ? this.element.value = newVal : this.element.value);
};
domElement.prototype.append = function(html) {
this.element.innerHTML = this.element.innerHTML + html;
};
domElement.prototype.prepend = function(html) {
this.element.innerHTML = html + this.element.innerHTML;
};
domElement.prototype.html = function(html) {
if(html === undefined){
return this.element.innerHTML;
}
this.element.innerHTML = html;
};
These are all pretty straight forward. The only one to pay attention to is .html(). This method can be invoked in two ways if it is called with no argument it will return the innerHTML for the element but if it is called with an argument it sets the HTML for the element. This is commonly refereed to as a getter / setter function.
Initialization
On initialization we need to do one of two things…
- If the selector starts with an open bracket ‘<’ we will create a new element.
- Otherwise we will use the document.querySelector to select an existing element.
For the purpose of simplicity, I am only doing the bare minimum in regards to validating the HTML in the case of creating an element and when selecting an element I am using document.querySelector meaning that it will only return a single element (the first match) regardless of the amount of matches.
This can be changed without too much effort to select all matching elements by using document.querySelectorAll and refactoring the methods to work with an element array.
domElement.prototype.init = function() {
switch(this.selector[0]){
case ‘<’ :
//create element
var matches = this.selector.match(/<([\w-]*)>/);
if(matches === null || matches === undefined){
throw ‘Invalid Selector / Node’;
return false;
}
var nodeName = matches[0].replace(‘<’,’’).replace(‘>’,’’);
this.element = document.createElement(nodeName);
break;
default :
this.element = document.querySelector(this.selector);
}
};
Lets walk through the above code.
- We use a switch statement, and pass the first character of our selector as the argument.
- If it begins with a bracket, we do a quick Regex match to find the text between the the open and close brackets. If this fails we throw an error that the selector is invalid.
- If a match is made, we strip out the brackets and pass the text to document.createElement to create a new element.
- Alternatively, we look for a match using document.querySelector this returns null if there is no match found.
- Lastly, we set the element property on our domElement to the matched / created element.
Using $ to reference domElement
Lastly we will assign the $ symbol to initialize a new domElement.
$ = function(selector){
var el = new domElement(selector); // new domElement
el.init(); // initialize the domElement
return el; //return the domELement
}
The $ symbol is just a variable! That’s our completed jQuery-like library, and all in 71 lines of readable, well spaced code.
Here’s a pen running the complete library…use your console.
What to do next?
- Why not try to replicate your favorite utility functions?
- Dive into the DOM
- Use event listeners to bind two-way data.
Important Notes
A special thanks to Quincy Larson for humanizing this post by fixing my butchery of the English language, visual tweaks and the great header image.
This code was written as a simple example to illustrate how JavaScript libraries interact with — and modify — the DOM. It should be treated as such.
I used simple, clear statements to help readers understand and follow the examples, and lightly skirted around — or completely ignored — points of failure and validation.
The returned element will only have the created methods bound to the wrapper. You can access the actual DOM element and its methods by calling $(‘selector’).element. This is to avoid extending the DOM, which is a sensitive topic requiring its own post.
If you followed the steps correctly, you should have a completed file like below:
var domElement = function(selector) {
this.selector = selector || null;
this.element = null;
};
domElement.prototype.init = function() {
switch (this.selector[0]) {
case ‘<’:
var matches = this.selector.match(/<([\w-]*)>/);
if (matches === null || matches === undefined) {
throw ‘Invalid Selector / Node’;
return false;
}
var nodeName = matches[0].replace(‘<’, ‘’).replace(‘>’, ‘’);
this.element = document.createElement(nodeName);
break;
default:
this.element = document.querySelector(this.selector);
}
};
domElement.prototype.on = function(event, callback) {
var evt = this.eventHandler.bindEvent(event, callback, this.element);
}
domElement.prototype.off = function(event) {
var evt = this.eventHandler.unbindEvent(event, this.element);
}
domElement.prototype.val = function(newVal) {
return (newVal !== undefined ? this.element.value = newVal : this.element.value);
};
domElement.prototype.append = function(html) {
this.element.innerHTML = this.element.innerHTML + html;
};
domElement.prototype.prepend = function(html) {
this.element.innerHTML = html + this.element.innerHTML;
};
domElement.prototype.html = function(html) {
if (html === undefined) {
return this.element.innerHTML;
}
this.element.innerHTML = html;
};
domElement.prototype.eventHandler = {
events: [],
bindEvent: function(event, callback, targetElement) {
this.unbindEvent(event, targetElement);
targetElement.addEventListener(event, callback, false);
this.events.push({
type: event,
event: callback,
target: targetElement
});
},
findEvent: function(event) {
return this.events.filter(function(evt) {
return (evt.type === event);
}, event)[0];
},
unbindEvent: function(event, targetElement) {
var foundEvent = this.findEvent(event);
if (foundEvent !== undefined) {
targetElement.removeEventListener(event, foundEvent.event, false);
}
this.events = this.events.filter(function(evt) {
return (evt.type !== event);
}, event);
}
};
$ = function(selector) {
var el = new domElement(selector);
el.init();
return el;
}
If you enjoyed this post take a look at some other stuff I’ve written.
5 Things to Remember When You’re Learning to Program
Turning code to cash — How to make money as a Web Developer and live to tell the tale.
How I Became a Programmer. And When I Started Calling Myself One