David Banks

Web development tips and tutorials

Build your own DOM utility (Part 3)

10 May 2015HTMLJavaScript

This post is the final part (part 3) of 'Building your own DOM utility'.

You can read Part 1 first to see my implementation of the $DOM utility, and Part 2 to see the implentation of several useful methods. This post adds a two more methods: css() for styling elements and data() for binding data to elements.

Method: css()

This will be a getter and a setter method. When we get the style of an element we will be using the window.getComputedStyle() function, which returns an object containing all computed style information on an element, whether we have set any inline styles or not. The setter versions will be chainable.

We will need a helper function for converting a dashed string to camel case. For example, converting 'background-color' to 'backgroundColor'. This is required because when we set inline styles using JavaScript, it is done like so:



elem.style.color = 'red';
elem.style.backgroundColor = 'white';
elem.style.fontSize = '2em';
// etc...

Now, to the implentation of our method:



$DOM_proto.css = function css() {

    // Helper function for converting dashed property names to camel case
    // e.g. background-color => backgroundColor
    function _toCamelCase (string) {

        return string.replace(/-([a-z]{1})/g, function (match, letter) {
            return letter.toUpperCase();
        });

    };

    // Helper function for applying an object of CSS styles to an element
    // Use `Object.prototype.hasOwnProperty.call()` instead of
    // `styles.hasOwnProperty()` as a safe check, in case someone decides to
    // set a custom property called `hasOwnProperty`!
    function _applyCss(elem, styles) {

        for (s in styles) {
            if (Object.prototype.hasOwnProperty.call(styles, s))
                elem.style[_toCamelCase(s)] = styles[s];
        }

    };

    var a = arguments,
        styles,
        returnVal = this;

    // Return all computed styles if no arguments provided
    if (a.length === 0)
        return window.getComputedStyle(this._element, null);

    if (a.length === 1)  {

        // Single argument of type string - retrieve the
        // computed value of that style property
        if (typeof a[0] === 'string')
            return window.getComputedStyle(this._element, null)[a[0]];

        // Single object argument - assume keys are css property names
        // and values are css property values.
        if (typeof a[0] === 'object') {
            _applyCss(this._element, a[0]);
            return this;
        }

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

    }

    // 2 arguments (or more) were provided
    // If first is not a string, throw error
    if (typeof a[0] !== 'string')
        throw new Error('Invalid CSS property specified');

    // Argument 1 is property name, argument 2 is the value to set
    var property = _toCamelCase(a[0]),
        value = a[1];

    this._element.style[property] = value;

    return this;

};

We have implemented this method so that it has a variety of different ways it can be called:



var myDiv = $DOM('#myDiv')[0];

// Get ALL computed styles
var allStyles = myDiv.css();

// Get a particular computed style
var fontSize = myDiv.css('font-size');

// Set a particular style
myDiv.css('color', 'navy');

// Set several styles at once
myDiv.css({
	color: 'navy',
	'font-weight': 'bold',
	'background-color': '#CCC'
});

Method: data()

Binding data to elements can be useful in some circumstances, such as when events fire on a list of similar elements and we want access to some data associated with each particular element within the event handler function. Or it can be used to record a previous state of an element so that we are able to restore that state later on.

We will implement this method as a getter and a setter. The setter versions will be chainable. We use a helper clone() function for this function (implemention not shown). This is crucial for preventing accidental modification of the data bound to the element. For instance, without cloning data that we set or retrieve, we can get potentially undesired or unexpected behaviour like this:



// Set some data
elem.data('items', [1,2,3]);

// ... more code ...

// Later on - want to retrieve data
var x = elem.data('items');

// We modify our retrieved array
x.push(4);

// But look what happens!
elem.data('items')  // => [1,2,3,4]

So now, with all this in mind, here is the implementation of data():



$DOM_proto.data = function data() {

    var dataProp = '_$DATA_', // where to store data on the element
        a = arguments;

    // Retrieve the data storage on the current element
    var currentData = this.prop(dataProp);

    // If it does not exist, create a new empty object
    if (!currentData) {
        currentData = Object.create(null);
        this.prop(dataProp, currentData);
    }

    // Return all data bound to the current element
    if (a.length === 0)
        return clone(currentData);

    // Remove all data bound to the current element
    if (a[0] === null) {
        this.prop(dataProp, null);
        return this;
    }

    // Set several data keys at once. Use safe method of  calling
    // `hasOwnProperty` in case the user decides for some reason
    // to set a data value using `hasOwnProperty` as the key.
    if (typeof a[0] === 'object') {

        for (key in a[0]) {
            if (Object.prototype.hasOwnProperty.call(a[0], key))
                currentData[key] = clone(a[0][key]);
        }

        return this;

    }

    // If first argument specified and not null or object, must be string
    if (typeof a[0] !== 'string')
        throw new Error('Data keys must be string values');

    // Retrieve a data value by key
    if (a.length === 1)
        return currentData ? clone(currentData[a[0]]) : undefined;

    // We want to set a single data key - only primitive types
   currentData[a[0]] = clone(a[1]);

    return this;

};

Just like with our css() implementation, we have writted the data() method so that it can be called in a variety of ways. There are 2 getter versions and 2 setter versions. For example:



var myDiv = $DOM('#myDiv')[0];

// Get all data as an object
var allData = myDiv.data();

// Get a particular value by key
var state = myDiv.data('state');

// Set a particular key and value
myDiv.data('state', 'inactive');

// Set multiple keys and values at once
myDiv.data({
	x: 200,
	y: 50,
	x: 3
});

That concludes the construction of our own DOM utility. There are of course other methods you may think are useful that I haven't included. Feel free to grab the code from my GitHub page (link at the top of this post) and try to implement your own methods if you like!