JS Hacks: Dead Simple Javascript Variable Change Watchers

Writing boilerplate to change variables, re-calculate aggregate values and update views sucks and is error-prone.

De-coupleing the management of your data with whatever other parts of your application that affects makes your application more flexible, more testable, and actually easier to understand because there’s less munging of concerns within a single function.

Most of the newer Javascript frameworks have some sort of pattern for variable binding, in Backbone it’s model change events [http://backbonejs.org/#Model] , in Ember it’s controller bindings [http://emberjs.com/documentation/#toc_setting-up-bindings] and Ember.Observable [http://emberjs.com/api/classes/Ember.Observable.html], in Angular [http://angularjs.org/] it’s done declaratively but in the HTML directly and the same is true with KnockoutJS [http://knockoutjs.com/].

However, if you feel like some of these solutions are overly complex, don’t dig the get()/set() syntax or are interested in building a purpose-driven micro-framework you might be interested to know that Javascript actually supports transparent getter/setter functions just like ruby, java, actionscript and others. Here is a simple trio of functions that exposes this to you in a cross-browser manner and even notifies you of modifications done on an array (IE9+):

function watch(target, prop, handler) {
    if (target.__lookupGetter__(prop) != null) {
        return true;
    }
    var oldval = target[prop],
        newval = oldval,
        self = this,
        getter = function () {
            return newval;
        },
        setter = function (val) {
            if (Object.prototype.toString.call(val) === '[object Array]') {
                val = _extendArray(val, handler, self);
            }
            oldval = newval;
            newval = val;
            handler.call(target, prop, oldval, val);
        };
    if (delete target[prop]) { // can't watch constants
        if (Object.defineProperty) { // ECMAScript 5
            Object.defineProperty(target, prop, {
                get: getter,
                set: setter,
                enumerable: false,
                configurable: true
            });
        } else if (Object.prototype.__defineGetter__ && Object.prototype.__defineSetter__) { // legacy
            Object.prototype.__defineGetter__.call(target, prop, getter);
            Object.prototype.__defineSetter__.call(target, prop, setter);
        }
    }
    return this;
};

function unwatch(target, prop) {
    var val = target[prop];
    delete target[prop]; // remove accessors
    target[prop] = val;
    return this;
};

// Allows operations performed on an array instance to trigger bindings
function _extendArray(arr, callback, framework) {
    if (arr.__wasExtended === true) return;

    function generateOverloadedFunction(target, methodName, self) {
        return function () {
            var oldValue = Array.prototype.concat.apply(target);
            var newValue = Array.prototype[methodName].apply(target, arguments);
            target.updated(oldValue, motive);
            return newValue;
        };
    }
    arr.updated = function (oldValue, self) {
        callback.call(this, 'items', oldValue, this, motive);
    };
    arr.concat = generateOverloadedFunction(arr, 'concat', motive);
    arr.join = generateOverloadedFunction(arr, 'join', motive);
    arr.pop = generateOverloadedFunction(arr, 'pop', motive);
    arr.push = generateOverloadedFunction(arr, 'push', motive);
    arr.reverse = generateOverloadedFunction(arr, 'reverse', motive);
    arr.shift = generateOverloadedFunction(arr, 'shift', motive);
    arr.slice = generateOverloadedFunction(arr, 'slice', motive);
    arr.sort = generateOverloadedFunction(arr, 'sort', motive);
    arr.splice = generateOverloadedFunction(arr, 'splice', motive);
    arr.unshift = generateOverloadedFunction(arr, 'unshift', motive);
    arr.__wasExtended = true;

    return arr;
}

Now you can register a handler that will be called every time any part of your code updates a bound variable. For example:

var data = {
     quantity: 0
     , products:  []
}
, watcher = function(propertyName, oldValue, newValue){ … update some other pieces of the application … };

watch(data, 'quantity', watcher);
watch(data, 'products', watcher);

If you’re stepping through your javascript console as you add these you can see that after each call to watch, the properties on data are redefined as getter/setters. So now, you can do things like:

data.quantity = 7;
data.products.push('beer')

and notice that your watcher function is called automatically with the new property values. Hooray!

Now your watcher function can manage propagating the changes to the rest of your application instead of you manually doing that in every place you might update your variables. If we take this one step further, we can get a [sweet way to de-couple concerns even further by providing multiple watch functions per variable - different blog post].

This code is adapted from @eligrey’s original gist which itself is originally modeled after Gecko’s built in watch() functionality.

1 comment

Leave a Reply

Your email address will not be published. Required fields are marked *

*

code

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>