Monitoring DOM properties

I recently came across a jQuery plugin that claims to emulate the functionality of Object.prototype.watch, a method inherent in all objects that enables you to watch for a property change and run a function upon that change. Of course, this isn’t available in all implementations. IE, for example, doesn’t support this but has a DOM equivalent, onPropertyChange. Both methods are useful but their slightly different functionality makes it impossible to use either of them to create a unified solution. Again we’re left to resort to the lowest common denominator, setInterval.

The jQuery plugin I mentioned attempts to use watch() if it’s available, otherwise it uses setInterval, repeatadly iterating through a stack of objects checking for changes. Even this attempt at a unified solution is futile because the functionality behind watch() is fundamentally unachievable with cross-browser JavaScript. According to this MDC documentation, the watch method not only acts as a type of event registrar for property-changes but also has the capability to define the property’s value (following the attempted change). In other words, the psuedo-event from watch() will fire before the property is actually changed, not after – the return value from the handler becomes the actual value of that property. Therefore, any attempts at emulation (for example, with setInterval) will not succeed since we’re only able to fire our psuedo-event after the change.

The following example shows how you would use watch() – at no time does the property actually change to ’456′, it’s constantly ’123′:

var obj = { prop: 123 };
 
obj.watch('prop', function(propertyName, oldValue, newValue){
    return oldValue;
});
 
obj.prop = 456;
 
alert(obj.prop); // 123

Additionally, watch() does not recognise internal property changes (within the DOM). If you setup a watch on the value property of an input element, even when you type into that field, your watch-handler will not execute. I’m not sure if this is intentional but it makes this technique entirely useless for monitoring DOM properties.

So, it appears that the only way to monitor DOM properties consistently across all browsers is to only use setInterval. The following plugins, watch and unwatch work quite well:

jQuery.fn.watch = function( id, fn ) {
 
    return this.each(function(){
 
        var self = this;
 
        var oldVal = self[id];
        $(self).data(
            'watch_timer',
            setInterval(function(){
                if (self[id] !== oldVal) {
                    fn.call(self, id, oldVal, self[id]);
                    oldVal = self[id];
                }
            }, 100)
        );
 
    });
 
    return self;
};
 
jQuery.fn.unwatch = function( id ) {
 
    return this.each(function(){
        clearInterval( $(this).data('watch_timer') );
    });
 
};
 
// Plus, I finally found use for jQuery.data()! ;)

These can be used on any type of object, an example:

$('input').watch('value', function(propName, oldVal, newVal){
    log('Value has been changed to ' + newVal);
});
 
// or...
 
var obj = { prop: 123 };
$(obj).watch('prop', function(propName, oldVal, newVal){
    log('Prop has been changed to ' + newVal);
});

I went ahead and created a new special event for jQuery using the plugin:

jQuery.fn.valuechange = function(fn) {
    return this.bind('valuechange', fn);
};
 
jQuery.event.special.valuechange = {
 
    setup: function() {
 
        jQuery(this).watch('value', function(){
            jQuery.event.handle.call(this, {type:'valuechange'});
        });
 
    },
 
    teardown: function() {
        jQuery(this).unwatch('value');
    }
 
};
 
// Usage:
$('input').bind('valuechange', function(){
    log('Value changed... New value: ' + this.value);
});

This event is useful when you want to be notified every single time the value of an input element changes. No native DOM event will do this.

If there’s one thiing you should take away from all this then it’s this: if you are going to try making a browser-specific feature available across all browsers you should make sure that you match the functionality precisely, otherwise it’s worthless. For example, an Array.prototype.forEach method that passes the index as the first parameter instead of the second is useless. If you can’t match the functionality then simply create your own abstraction.