TypeOverflow

Cover image for Make your own jQuery from scratch
Joydeep Bhowmik
Joydeep Bhowmik

Posted on • Updated on

Make your own jQuery from scratch

jQuery is one of the most popular JavaScript library . It greatly simplifies JavaScript programming. You might be curious about how jQuery is made. If you look through the source code of any well-known library, it's likely that you won't understand anything or you may feel that you need to be a genius to build one. However, it's not as difficult as it seems in reality. In this article I'm going show you how you can make your own library like jQuery from scratch.

Lets get started!

Creating the selector function

const $ = function(el) {
    return new jQuery(el);
}
Enter fullscreen mode Exit fullscreen mode

As you can see the selector function returns a class named jQuery. Every time you use the selector a new instance of jQuery will be created .

Now lets create our jQuery Class

jQuery object

 class jQuery extends Array{
    constructor(el) {
        super();
        if (el == document || el instanceof HTMLElement) {
            this.push(el);
        } else {
            let elements=document.querySelectorAll(el);
            for(let i=0;i<elements.length;i++){
                this.push(elements[i]);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Our jQuery class extends the build-in Array class so every instance behave like an array and an object at the same time.Every time we create a new jQuery instance it checks if the element is html element or if it is the document itself. If not then it performs a querySelectorAll to get the all matched elements. Then it pushes each element to itself. Now we can add our methods to it.

Chaining

The best thing I like about jQuery is that you can chain methods, like

$('div').addClass('red').html('hello');
Enter fullscreen mode Exit fullscreen mode

to apply Chaining to our jQuery class we just need to return the current instance, i.e adding
return this after every method.

ready()

ready(callback){
    this[0].addEventListener('readystatechange', e => {
        if(this[0].readyState === "complete"){
            callback();
            return true;
        }
      });
}
Enter fullscreen mode Exit fullscreen mode
$(document).ready(function(){
 //codes
})
Enter fullscreen mode Exit fullscreen mode

Checks the readyStates of an element if the readyState is complete than we know the element is completely loaded and ready for use;

each()

    each(callback) {
        if (callback && typeof(callback) == 'function') {
            for (let i = 0; i < this.length; i++) {
                callback(this[i], i);
            }
            return this;
        } 
    }
Enter fullscreen mode Exit fullscreen mode
$('div').each(function(el){
    $(el).html('text')
})
Enter fullscreen mode Exit fullscreen mode

this method iterate over all elements and give a callback for each element. We will use this for creating other methods.

siblings()

siblings(){
    return  [...this[0].parentNode.children].filter(c=>c!=this[0])
}
Enter fullscreen mode Exit fullscreen mode
console.log($('script').siblings())
Enter fullscreen mode Exit fullscreen mode

It's a pretty simple one liner. We first select all the child elements of parent element and then just remove the current element from the list , resulting an array of sibling elements.

addClass()

addClass(className) {
    this.each(function(el) {
        el.classList.add(className);
    })
    return this;
}
Enter fullscreen mode Exit fullscreen mode
$('div').addClass('pink')
Enter fullscreen mode Exit fullscreen mode

we use classList.add() to add the className to each item with the help of this.each method.

removeClass()

removeClass(className) {
    this.each(function(el) {
        el.classList.remove(className);
    })
    return this;
}
Enter fullscreen mode Exit fullscreen mode
$('div').removeClass('pink')
Enter fullscreen mode Exit fullscreen mode

similar to addClass we use classList.remove() to remove the className .

hasClass()


hasClass((className) {
    return this[0].classList.contains(className);
}
Enter fullscreen mode Exit fullscreen mode
console.log($('div').hasClass('pink')) //returns true or false
Enter fullscreen mode Exit fullscreen mode

hasClass check if the 1st selected element has any specific class with the help of classList.contains() and return true or false accordingly.

css()

 css(propertyObject) {
       this.each(function(el) {
           Object.assign(el.style,propertyObject);
        })
        return this;
    }
Enter fullscreen mode Exit fullscreen mode
$('div').css({
    color:'red'
})
Enter fullscreen mode Exit fullscreen mode

in this method the parameter is a type of object. each property of this object is added to the element.style object using in-build Object.assign function()

attr()

attr(attr, value = null) {
    let getattr = undefined;
    if (value) {
        this.each(function(el) {
            el.setAttribute(attr, value);
            getattr = this;
        });
    } else {
            getattr = this[0].getAttribute(attr);
    }
    return getattr;
}
Enter fullscreen mode Exit fullscreen mode
console.log($('div').attr('class')) //gets the attribute value
$('div').attr('name','div') //sets the name attribute with value
Enter fullscreen mode Exit fullscreen mode

If the value argument is not present then we just simple return the attribute value else we set the attribute for each element.

html()

html(data) {
    if (data) {
        this.each(function(el) {
            el.innerHTML = data;
        })
    } else {
        return this[0].innerHTML;
    }
    return this;
}
Enter fullscreen mode Exit fullscreen mode
console.log($('div').html()) //gets the innerHTML 
$('div').html('x') //add the innerHTML
Enter fullscreen mode Exit fullscreen mode

Similar to attr() method just uses innerHTML instead of attribute

prepend()

prepend(el){
    this[0].prepend(el);
    return this;
}
Enter fullscreen mode Exit fullscreen mode
let h1=document.createElement('h1');
h1.innerHTML='this is a heading';
$('div').prepend(h1) //prepend the node
$('div').prepend('hello') //prepend the string 
Enter fullscreen mode Exit fullscreen mode

prepend a node or string to the selected elements using in-build node.prepend().

append()

append(el){
    this[0].append(el);
    return this;
}
Enter fullscreen mode Exit fullscreen mode
let h1=document.createElement('h1');
h1.innerHTML='this is a heading';
$('div').append(h1) //append the node
$('div').append('hello') //append the string
Enter fullscreen mode Exit fullscreen mode

append a node or string to the selected elements using in-build node.append().

hide()

hide() {
    this.each(function(el) {
        el.style.display = "none";
    });
    return this;
}
Enter fullscreen mode Exit fullscreen mode
$('div').hide()
Enter fullscreen mode Exit fullscreen mode

change the style.display property to none

show()

show() {
    this.each(function(el) {
        el.style.display = "block";
    });
    return this;
}
Enter fullscreen mode Exit fullscreen mode
$('div').show()
Enter fullscreen mode Exit fullscreen mode

change the style.display property to block

on()

on(event, child, callback = null) {
    if (callback != null) {
        let selector = child;
        this.each(function(element) {
            element.addEventListener(event, function(event) {
                if (event.target.matches(selector + ', ' + selector + ' *')) {
                    callback.apply(event.target.closest(selector), arguments);
                }
            })
        })
    } else {
    //if the callback argument is not present then assume  the child argument is being use as callback
        callback = child;
        this.each(function(element) {
        element.addEventListener(event, callback);
        })
    }

    return this;
}
Enter fullscreen mode Exit fullscreen mode
$('div').on('click',function(){
    alert('x')
})
$(document).on('click','div',function(){
    alert('x')
})
Enter fullscreen mode Exit fullscreen mode

this one is little bit complex.
If we pass all three arguments to the method it will add an EventListener to the parent element and only if the target matches child element then will give a callback.

but if we just pass two argument then it will simply add an EventListener to the element with a callback.

for the first case we can take this as an example

$(document).on('click','div',function(){
    alert('x')
})
Enter fullscreen mode Exit fullscreen mode

and for the second case

$('div').on('click',function(){
    alert('x')
})
Enter fullscreen mode Exit fullscreen mode

both serves their different purpose . First one is very useful for binding event to dynamic element.
Second one is useful for general event binding .

$.ajax()

$.ajax=function(args) {
        let url = args["url"];
        let type = "get";
        let success =function(){};
        let fail = function(){};
        if(args['success']){
            success=args['success'];
        }
        if(args['fail']){
            fail=args['fail'];
        }
        let xhttp = new XMLHttpRequest();
        xhttp.onerror = function(error){
        //return fial callback with error 
            return fail(error);
        }
        xhttp.onload = function() {
            let response;
            if (this.readyState == 4 && this.status == 200) {
                let response="";
                try {
                //check if the response in json 
                //if json the parse it
                    response=JSON.parse(this.responseText)
                } catch (e) {
                //if not json the simple reurn the response
                    response = this.responseText;
                    }
                 // give a success callback
                return success(response);
            } else {
            //give a fail callback with the error status 
                return fail(this.status);
            }
        };
        let parameters="";
        if (args) {
            type = args["type"];
            if ('data' in args) {
            //converting object to url URLSearchParams
                parameters = new URLSearchParams(args['data']).toString();
            }

        }
        if (type && type.toUpperCase()=='POST') {
            xhttp.open("POST", url, true);
            xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhttp.send(parameters);
        } else if (!type || type.toUpperCase()=='GET'){
            xhttp.open("GET", url + "?" + parameters, true);
            xhttp.setRequestHeader("X-Requested-With", "XMLHttpRequest");
            xhttp.send();
        }
    }
Enter fullscreen mode Exit fullscreen mode
$.ajax({
    url:'post.php',
    type:'post',
    data:{
        name:'hello'
    },
    success: function(res){
        console.log(res)
    }
})
Enter fullscreen mode Exit fullscreen mode

this ajax function is not a method but works as a property of $ object.
this function takes url,type,data,success and fail arguments just like the original jQuery.
What we do is that we create a new new XMLHttpRequest() each time the function is called and send post or get request. Now if we receive the response then we check if it is JSON response.For JSON response we parse it otherwise just simply return a success callback with the response.But if request gives a error, fail callback with the status code will be returned.

Putting it all together

class jQuery extends Array{
    constructor(el) {
        super();
        if (el == document || el instanceof HTMLElement) {
            this.push(el);
        } else {
            let elements=document.querySelectorAll(el);
            for(let i=0;i<elements.length;i++){
                this.push(elements[i]);
            }
        }
    }
    ready(callback){
        this[0].addEventListener('readystatechange', e => {
            if(this[0].readyState === "complete"){
                callback();
                return true;
            }
          });
    }
    each(callback) {
        if (callback && typeof(callback) == 'function') {
            for (let i = 0; i < this.length; i++) {
                callback(this[i], i);
            }
            return this;
        } 
    }
    siblings(){
        return  [...this[0].parentNode.children].filter(c=>c!=this[0])
    }
    addClass(className) {
        this.each(function(el) {
            el.classList.add(className);
        })
        return this;
    }

    removeClass(className) {
        this.each(function(el) {
            el.classList.remove(className);
        })
        return this;
    }

    hasClass(className) {
        return this[0].classList.contains(className);
    }

    css(propertyObject) {
        this.each(function(el) {
            Object.assign(el.style,propertyObject);
        })
        return this;
    }

    attr(attr, value = null) {
        let getattr = undefined;
        if (value) {
            this.each(function(el) {
                el.setAttribute(attr, value);
                getattr = this;
            });
        } else {
            getattr = this[0].getAttribute(attr);
        }
        return getattr;
    }

    html(data) {
        if (data) {
            this.each(function(el) {
                el.innerHTML = data;
            })
        } else {
            return this[0].innerHTML;
        }
        return this;
    }
    append(el){
        this[0].append(el);
        return this;
    }
    prepend(el){
        this[0].prepend(el);
        return this;
    }
    hide() {
        this.each(function(el) {
            el.style.display = "none";
        });
        return this;
    }
    show() {
        this.each(function(el) {
            el.style.display = "block";
        });
        return this;
    }
    on(event, child, callback = null, state = null) {
        if (callback != null) {
            let selector = child;
            this.each(function(element) {
                element.addEventListener(event, function(event) {
                    if (event.target.matches(selector + ', ' + selector + ' *')) {
                        callback.apply(event.target.closest(selector), arguments);
                    }
                }, false)
            })
        } else {
            callback = child;
            this.each(function(element) {
                if (state) {
                    element.addEventListener(event, callback, state);
                } else {
                    element.addEventListener(event, callback, false);
                }
            })
        }

        return this;
    }
}
const $ = function(el) {
    return new jQuery(el);
}
$.ajax=function(args) {
        let url = args["url"];
        let type = "get";
        let success =function(){};
        let fail = function(){};
        if(args['success']){
            success=args['success'];
        }
        if(args['fail']){
            fail=args['fail'];
        }
        let xhttp = new XMLHttpRequest();
        xhttp.onerror = function(error){
            return fail(error);
        }
        xhttp.onload = function() {
            let response;
            if (this.readyState == 4 && this.status == 200) {
                let response="";
                try {
                    response=JSON.parse(this.responseText)
                } catch (e) {
                    response = this.responseText;
                    }
                return success(response);
            } else {
                return fail(this.status);
            }
        };
        let parameters="";
        if (args) {
            type = args["type"];
            if ('data' in args) {
                parameters = new URLSearchParams(args['data']).toString();
            }

        }
        if (type && type.toUpperCase()=='POST') {
            xhttp.open("POST", url, true);
            xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhttp.send(parameters);
        } else if (!type || type.toUpperCase()=='GET'){
            xhttp.open("GET", url + "?" + parameters, true);
            xhttp.setRequestHeader("X-Requested-With", "XMLHttpRequest");
            xhttp.send();
        }
    }
Enter fullscreen mode Exit fullscreen mode

Now you have your own jQuery library. Of course the Original jQuery have more methods and good optimization but this is enough for regular tasks.

IF you are interested in this type of posts please check out my
blog

Read next
Virtual Dom Diffing Algorithm Implementation In Vanilla JavaScript

Top comments (0)