David Banks

Web development tips and tutorials

Build your own DOM utility (Part 2)

1 May 2015HTMLJavaScript

This post is Part 2 of 'Building your own DOM utility'.

You can read Part 1 first to see my implementation of the $DOM utility. This post follows on from that by showing how to add further useful methods.

In order to make our custom $DOM utility more useful, we should add further methods to help perform common tasks with the DOM. Below are the details of how to implement various methods, many of which will be familiar to those who have used jQuery before.

Class-oriented methods

First up, we'll create some methods for dealing with adding, removing and checking for the presence of CSS classes on elements. Each DOM element has a className property which stores all classes currently set on that element as a string, with a single space between each class name. Knowing this fact, it is fairly easy to implement class-oriented methods.

Method: hasClass()

This method checks to see whether the element has a given class.



$DOM_proto.hasClass = function hasClass(className) {

    // Error if class name not specified
    if (!className)
        throw new Error('No class name specified');

    var currentClasses = this._element.className.split(' ');

    return currentClasses.indexOf(className) > -1;

};

Method: addClass()

This method adds a given class name to the current element if it is not already present. This method is chainable.



$DOM_proto.addClass = function addClass(className) {

    // Error if no class name specified
    if (!className)
        throw new Error('No class name specified')

    var currentClasses = this._element.className.split(' ');

    if (!this.hasClass(className))
        currentClasses.push(className);

    this._element.className = currentClasses.join(' ').trim();
    return this;

};

Method: removeClass()

This method removes a given class name from the current element if it is present on the element. This method is chainable.



$DOM_proto.removeClass = function removeClass(className) {

    // Error if no class name specified
    if (!className)
        throw new Error('No class name specified')

    var classList = this._element.className

    if (this.hasClass(className))
        classList = classList.replace(className, '');

    // Update className property, removing excess white space
    this._element.className = classList.replace(/\s+/g, ' ').trim();
    return this;

};

Method: toggleClass()

This method toggles a given class name on the element. If it is currently set, it is removed, otherwise it is added to the element. This method is chainable.



$DOM_proto.toggleClass = function toggleClass(className) {

    // Error if class name not specified
    if (!className)
        throw new Error('No class name specified')

    if (this.hasClass(className)) {
        this.removeClass(className);
    } else {
        this.addClass(className);
    }

    return this;

};

Ancestor and descendant methods

Next we'll implement methods for getting the parent element, getting direct children elements, and selecting elements using the current element as the root element.

Method: parent()

This method gets the parent element of the current element if it exists, otherwise it returns null. It simply accesses the parentNode property on the standard DOM element. Returns the parent element as a $DOM object.



$DOM_proto.parent = function parent() {

    return (this._element.parentNode ?
        $DOM(this._element.parentNode) :
        null);

};

Method: children()

This method gets the direct children elements of the current element, by using the children property present on all standard DOM elements. Returns an array of $DOM corresponding to the children elements.



$DOM_proto.children = function children() {

    var c = this._element.children,
        $doms = [],
        i, n = c.length;

    // Convert to $DOM objects
    for (i = 0; i < n; i++)
        $doms[i] = $DOM(c[i]);

    return $doms;;

};

Method: find()

This method returns all descendant elements (at any level) that match the provided selector. It does this by calling querySelectorAll on the current element instead of on document. Returns an array of matching $DOM objects.



$DOM_proto.find = function find(selector) {

    var elems;

    try {
        elems = this._element.querySelectorAll(selector);
    } catch (exception) {
        throw new Error('Invalid selector provided');
    }

    var i, n = elems.length,
        $doms = [];

    // Convert to $DOM objects
    for (i = 0; i < n; i++)
        $doms.push($DOM(elems[i]));

    return $doms;

};

Method: append()

This method appends an element as a last child of the current element. We will accept three types of argument: a $DOM object, a raw DOM element, or a string specifying that we want to create a new element on the fly (e.g. <div>). Returns the appended element as a $DOM object.



$DOM_proto.append = function append(child) {

    // Error for no argument
    if (!child)
        throw new Error('No argument provided');

    // Raw DOM element
    if (child instanceof Element) {
        this._element.appendChild(child);
        return $DOM(child);
    }

    // Custom $DOM object
    if (child._element instanceof Element) {
        this._element.appendChild(child._element);
        return child;
    }

    var newTagPattern = /^<([^\/]+)\/?>$/,
        elem;

    // Create a new element on the fly
    if (newTagPattern.test(child)) {

        elem = document.createElement(child.match(newTagPattern)[1]);
        this._element.appendChild(elem);
        return $DOM(elem);

    }

    // Otherwise throw error
    throw new Error('Invalid argument provided');

};

Method: prepend()

This method prepends an element as a first child of the current element. We will accept three types of argument: a $DOM object, a raw DOM element, or a string specifying that we want to create a new element on the fly (e.g. <div>). Returns the prepended element as a $DOM object.



$DOM_proto.prepend = function prepend(child) {

    // Helper function to avoid code repetition
    var self = this;
    var _insertBefore = function(elem) {
        self._element.insertBefore(elem, self._element.childNodes[0]);
    };

    // Error for no argument
    if (!child)
        throw new Error('No argument provided');

    // Raw DOM element
    if (child instanceof Element) {
        _insertBefore(child);
        return $DOM(child);
    }

    // Custom $DOM object
    if (child._element instanceof Element) {
        _insertBefore(child._element);
        return child;
    }

    var newTagPattern = /^<([^\/]+)\/?>$/,
        elem;

    // Create a new element on the fly
    if (newTagPattern.test(child)) {

        elem = document.createElement(child.match(newTagPattern)[1]);
        _insertBefore(elem)
        return $DOM(elem);

    }

    // Otherwise throw error
    throw new Error('Invalid argument provided');

};

Method: remove()

Removes the current element from the document. This method is chainable, but note that any further operations will not be visible unless the element is later added back into the document.



$DOM_proto.remove = function remove() {

    var parent = this.parent();

    // Throw error if element is document root
    if (!parent)
        throw new Error('Cannot remove root element!');

    parent._element.removeChild(this._element);

    return this;

}

Add event listeners

All modern browsers support addEventListener, so this is what we we will use in our method for adding event listeners to the current element.

Method: on()

Add an event listener to the current element. The third argument context allows you to use a custom value for this inside the event handler function. This method is chainable.



$DOM_proto.on = function on(eventName, handler, context) {

    if (typeof context === 'undefined')
        context = this;

    this._element.addEventListener(eventName, handler.bind(context), false);

    return this;

};

I hope you enjoyed this post. Most of the convenience methods have now been implemented. We have still not implemented a css() method for adding inline styles, so watch for Part 3 of this post coming soon. See Part 3 next, where we will also look at a data() method for binding data to elements.