user17753
user17753

Reputation: 3161

how to code around storing data (objects, custom attributes) on DOM nodes

I wanted to write some pure javascript to better understand it (I realize in "real practice" that frameworks such as jQuery are much more advised and applicable, but this isn't really about how to use frameworks, more about how pure javascript works and best practices).

Anyways, I wrote some simple javascript code. I wanted to create a set of groups of buttons that had one state at a time from the set {on,off} and each state would map to a corresponding function to be fired upon entering that state. Each group of buttons within the master set could contain only one button in the on state at a time. The concept is similar to the idea of radio buttons. Why not use a radio button then? Well semantically it's just suppose to be some buttons for some control elements, but either way I suppose I could have but the question isn't really about that.

The thing is, to pull this off, I added a lot of custom attributes to specific button elements by id in my Javascript. I was doing some research, and found this question and this question, regarding using custom attributes on DOM node (objects). They seem to advocate against such a practice, one even goes so far as to say that doing so could cause potential memory leaks depending on the browser's implementation.

However, for each button I create I need to keep track of a lot of attributes, and if I expand this script I may have to add even more. So what's the best way around storing them on the DOM node but still keeping track of everything and being able to use this in attached functions, etc. al?

It wasn't readily obvious to me how to do this without at the minimum storing a reference of a well name spaced object to the DOM node button element.

I was able to see that from this question jQuery has some way to do this, but I want to know how this is done with just pure javascript.

Here's the full sample code I am working with:

<!DOCTYPE html>
<html>
<head>
    <title>Button Test Script</title>

<script language="javascript" type="text/javascript">

window.button_groups = {};

function isset( type ) {
    return !(type==='undefined');   
}

function debug( txt ) { 
    if( !isset(typeof console) ) {
        alert( txt );   
    } else {
        console.log(txt);
    }
}

function img( src ) {
    var t = new Image();
    t.src = src;
    return t;       
}

function turnGroupOff( group ) {
    if( isset(typeof window.button_groups[group]) ) {               
        for( var i = 0; i < window.button_groups[group].length; i++ ) {         
            if( window.button_groups[group][i].toggle == 1)
                window.button_groups[group][i].click();
        }               
    }
}
/**
 * buttonId = id attribute of <button>
 * offImg = src of img for off state of button
 * onImg = src of img for on state of button
 * on = function to be fired when button enters on state
 * off = function to be fired when button enters off state
 */
function newButton( buttonId, offImg, onImg, group, on, off ) {

    var b = document.getElementById(buttonId);
    b.offImg = img( offImg );
    b.onImg = img( onImg );
    b.on = on;
    b.off = off;
    b.img = document.createElement('img');  
    b.appendChild(b.img);
    b.img.src = b.offImg.src;
    b.group = group;

    b.toggle = 0;

    b.onclick = function() {
        switch(this.toggle) {
        case 0:                                             
            turnGroupOff( this.group );
            this.on();
            this.toggle = 1;
            this.img.src = this.onImg.src;
            break;
        case 1:
            this.off();
            this.toggle = 0;
            this.img.src = this.offImg.src;
            break;
        }       
    }

    if( !isset(typeof window.button_groups[group]) )
        window.button_groups[group] = [];
    window.button_groups[group].push(b);                    

}


function init() {

    var on = function() { debug(this.id + " turned on") };

    newButton( 'button1', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group1',
        on,
        function() { debug(this.id + " turned off"); }
        );
    newButton( 'button2', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group1',
        on,
        function() { debug(this.id + " turned off (diff then usual turn off)"); }
        );

    newButton( 'button3', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group2',
        on,
        function() { debug(this.id + " turned off (diff then usual turn off2)"); }
        );
    newButton( 'button4', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group2',
        on,
        function() { debug(this.id + " turned off (diff then usual turn off3)"); }
        );

}

window.onload = init;
</script>

</head>

<body>


<button id="button1" type="button"></button>
<button id="button2" type="button"></button>
<br/>
<button id="button3" type="button"></button>
<button id="button4" type="button"></button>

</body>
</html>

UPDATE

The jQuery thing was a bit overkill for my purposes. I don't need to extend an arbitrary element. I have a good idea of how that is done now specific to jQuery (with the arbitrary randomly named attribute storing a cache index integer).

I know ahead of time which host elements I need to extend, and how; also I can/want to setup an id attribute on each of them on the HTML side.

So, inspired by the jQuery setup, I decided to also create a global cache variable except I am going to use the DOM node's id attribute as my cache key. Since it should be a unique identifier (by definition), and I have no plans to dynamically alter id's ever, this should be a simple task. It completely divorces my Javascript objects from the DOM objects, but it does make my code look quite a bit uglier and difficult to read with the many calls to data. I present the modifications below:

<!DOCTYPE html>
<html>
<head>
    <title>Button Test Script</title>
<script language="javascript" type="text/javascript">

window.button_groups = {};

function isset( type ) { // For browsers that throw errors for !object syntax
    return !(type==='undefined');   
}

var c = { // For browsers without console support
    log: function( o ) {
        if( isset(typeof console) ) {
            console.log(o); 
        } else {
            alert( o );
        }
    },
    dir: function( o ) { 
        if( isset(typeof console) ) {
            console.dir(o);
        }
    }
};

function img( src ) { // To avoid repeats of setting new Image src
    var t = new Image();
    t.src = src;
    return t;       
}

var cache = {};
function data( elemId, key, data ) { // retrieve/set data tied to element id

    if(isset(typeof data)) {// setting data         
        if(!isset(typeof cache[elemId]))
            cache[elemId] = {};
        cache[elemId][key] = data;

    } else { // retreiving data
        return cache[elemId][key];      
    }   

}

var button_groups = {}; // set of groups of buttons

function turnGroupOff( group ) { // turn off all buttons within a group
    if( isset(typeof window.button_groups[group]) ) {               
        for( var i = 0; i < window.button_groups[group].length; i++ ) {         
            if( data(window.button_groups[group][i].id, 'toggle') == 1)
                window.button_groups[group][i].click();
        }               
    }
}


/**
 * buttonId = id attribute of <button>
 * offImg = src of img for off state of button
 * onImg = src of img for on state of button
 * on = function to be fired when button enters on state
 * off = function to be fired when button enters off state
 */
function newButton( buttonId, offImg, onImg, group, on, off ) {

    var b = document.getElementById(buttonId);  
    data( b.id, 'offImg', img( offImg ) );
    data( b.id, 'onImg', img( onImg ) );
    data( b.id, 'on', on );
    data( b.id, 'off', off );   
    var btnImg = document.createElement('img');
    btnImg.src = data( b.id, 'offImg' ).src;
    data( b.id, 'img', btnImg  );
    b.appendChild( btnImg );
    data( b.id, 'group', group );
    data( b.id, 'toggle', 0 );

    var click = function() {
        switch(data(this.id,'toggle')) {
        case 0:                                             
            turnGroupOff( data(this.id,'group') );
            (data(this.id,'on'))();
            data(this.id,'toggle',1);
            data(this.id,'img').src = data(this.id,'onImg').src;
            break;
        case 1:
            (data(this.id,'off'))();
            data(this.id,'toggle',0);
            data(this.id,'img').src = data(this.id,'offImg').src;
            break;
        }   

    }

    b.onclick = click;

    if( !isset(typeof window.button_groups[group]) )
        window.button_groups[group] = [];
    window.button_groups[group].push(b);                    
}


function init() {

    var on = function() { c.log(this.id + " turned on") };

    newButton( 'button1', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group1',
        on,
        function() { c.log(this.id + " turned off"); }
        );
    newButton( 'button2', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group1',
        on,
        function() { c.log(this.id + " turned off (diff then usual turn off)"); }
        );

    newButton( 'button3', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group2',
        on,
        function() { c.log(this.id + " turned off (diff then usual turn off2)"); }
        );
    newButton( 'button4', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group2',
        on,
        function() { c.log(this.id + " turned off (diff then usual turn off3)"); }
        );

}


window.onload = init;


</script>       
</head>

<body>


<button id="button1" type="button"></button>
<button id="button2" type="button"></button>
<br/>
<button id="button3" type="button"></button>
<button id="button4" type="button"></button>


</body>
</html>

UPDATE 2

I found that through using the power of closure I truly only need to store one "special" attribute, that is the group the button belonged to.

I changed the newButton function to the following, which through closure, eliminates the need to store many of those other things I was:

function newButton( buttonId, offImg, onImg, group, on, off ) {

    var b = document.getElementById(buttonId);  
    offImg = img( offImg );
    onImg = img( onImg );
    var btnImg = document.createElement('img');
    btnImg.src = offImg.src;
    b.appendChild( btnImg );
    data( b.id, 'group', group );
    var toggle = 0;

    var click = function(event) {
        switch(toggle) {
        case 0:                                             
            turnGroupOff( data(this.id,'group') );
            if( on(event) ) {
                toggle = 1;
                btnImg.src = onImg.src;
            }
            break;
        case 1:
            if( off(event) ) {
                toggle = 0;
                btnImg.src = offImg.src;
            }
            break;
        }   

    }

    b.onclick = click;

    if( !isset(typeof window.button_groups[group]) )
        window.button_groups[group] = [];
    window.button_groups[group].push(b);    

    b = null;
}

Upvotes: 0

Views: 1099

Answers (2)

Matt Esch
Matt Esch

Reputation: 22956

You either extend objects (which is bad for host objects) or you wrap the objects as jQuery does, using the wrapped object to identify associated data in the hash table. In essence you hash the DOM node and do a lookup in a hash table for the associated data. Of course you still need to extend the host object, but you add only a single property which you know to be reasonably safe to add across browsers, rather than a set of arbitrary properties. If you inspect an element with associated data you might see something like element.jQuery171023696433915756643, which contains the internal storage index for that element. I would recommend reading the jQuery source if you are that interested, particularly the data() function

data: function( elem, name, data, pvt /* Internal Use Only */ ) {
        if ( !jQuery.acceptData( elem ) ) {
            return;
        }

        var privateCache, thisCache, ret,
            internalKey = jQuery.expando,
            getByName = typeof name === "string",

            // We have to handle DOM nodes and JS objects differently because IE6-7
            // can't GC object references properly across the DOM-JS boundary
            isNode = elem.nodeType,

            // Only DOM nodes need the global jQuery cache; JS object data is
            // attached directly to the object so GC can occur automatically
            cache = isNode ? jQuery.cache : elem,

            // Only defining an ID for JS objects if its cache already exists allows
            // the code to shortcut on the same path as a DOM node with no cache
            id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey,
            isEvents = name === "events";

        // Avoid doing any more work than we need to when trying to get data on an
        // object that has no data at all
        if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) {
            return;
        }

        if ( !id ) {
            // Only DOM nodes need a new unique ID for each element since their data
            // ends up in the global cache
            if ( isNode ) {
                elem[ internalKey ] = id = ++jQuery.uuid;
            } else {
                id = internalKey;
            }
        }

        if ( !cache[ id ] ) {
            cache[ id ] = {};

            // Avoids exposing jQuery metadata on plain JS objects when the object
            // is serialized using JSON.stringify
            if ( !isNode ) {
                cache[ id ].toJSON = jQuery.noop;
            }
        }

        // An object can be passed to jQuery.data instead of a key/value pair; this gets
        // shallow copied over onto the existing cache
        if ( typeof name === "object" || typeof name === "function" ) {
            if ( pvt ) {
                cache[ id ] = jQuery.extend( cache[ id ], name );
            } else {
                cache[ id ].data = jQuery.extend( cache[ id ].data, name );
            }
        }

        privateCache = thisCache = cache[ id ];

        // jQuery data() is stored in a separate object inside the object's internal data
        // cache in order to avoid key collisions between internal data and user-defined
        // data.
        if ( !pvt ) {
            if ( !thisCache.data ) {
                thisCache.data = {};
            }

            thisCache = thisCache.data;
        }

        if ( data !== undefined ) {
            thisCache[ jQuery.camelCase( name ) ] = data;
        }

        // Users should not attempt to inspect the internal events object using jQuery.data,
        // it is undocumented and subject to change. But does anyone listen? No.
        if ( isEvents && !thisCache[ name ] ) {
            return privateCache.events;
        }

        // Check for both converted-to-camel and non-converted data property names
        // If a data property was specified
        if ( getByName ) {

            // First Try to find as-is property data
            ret = thisCache[ name ];

            // Test for null|undefined property data
            if ( ret == null ) {

                // Try to find the camelCased property
                ret = thisCache[ jQuery.camelCase( name ) ];
            }
        } else {
            ret = thisCache;
        }

        return ret;
    }

Upvotes: 1

Manatok
Manatok

Reputation: 5706

I found this article on javascript design patterns that may give you some ideas. Have a look at The Prototype Pattern, this allows you to reuse methods across instances.

Upvotes: 1

Related Questions