Saturday, January 10, 2009

Internet Explorer Object watch

Update
As Luke suggested, there is a method remove in the watcher that should solver leaks problem while the new version should be supported by most recent Opera, Safari, FireFox, and Internet Explorer.
Main updates:

  • the watcher is always another object and not the original one

  • to safely destroy the watcher use its destroy method and then delete it


----------------------------------------------


We all complain about IE and its non-standard attitude, this time I would like to introduce a non standard feature, as Object.prototype.watch is, for Internet Explorer.

What is watch and why we need it


The watch method allows validation, control, monitoring, over a generic object.
This means that we can control properties assignment with one, or more, callbacks.

var man = {};
man.watch("name", function(propertyName, oldValue, newValue){
// propertyName: name
// oldValue: undefined if it is the first time, the old one otherwise
// newValue: new assigned value
return "Mr " + newValue;
});

man.name = "Andrea";
alert(man.name); // Mr Andrea


watch for Internet Explorer, about my implementation


I used a weird strategy to implement this feature in Internet Explorer and it is based on DOM and its IE feature called onpropertychange.
Accordingly, it was not possible to create an Object.prototype.watch, while it was simple to implement a createWatcher callback.

(function(watch, unwatch){
createWatcher = watch && unwatch ?
// Andrea Giammarchi - Mit Style License
function(Object){
var handlers = [];
return {
destroy:function(){
handlers.forEach(function(prop){
unwatch.call(this, prop);
}, this);
delete handlers;
},
watch:function(prop, handler){
if(-1 === handlers.indexOf(prop))
handlers.push(prop);
watch.call(this, prop, function(prop, prevValue, newValue){
return Object[prop] = handler.call(Object, prop, prevValue, newValue);
});
},
unwatch:function(prop){
var i = handlers.indexOf(prop);
if(-1 !== i){
unwatch.call(this, prop);
handlers.splice(i, 1);
};
}
}
}:(Object.prototype.__defineSetter__?
function(Object){
var handlers = [];
return {
destroy:function(){
handlers.forEach(function(prop){
delete this[prop];
}, this);
delete handlers;
},
watch:function(prop, handler){
if(-1 === handlers.indexOf(prop))
handlers.push(prop);
if(!this.__lookupGetter__(prop))
this.__defineGetter__(prop, function(){return Object[prop]});
this.__defineSetter__(prop, function(newValue){
Object[prop] = handler.call(Object, prop, Object[prop], newValue);
});
},
unwatch:function(prop){
var i = handlers.indexOf(prop);
if(-1 !== i){
delete this[prop];
handlers.splice(i, 1);
};
}
};
}:
function(Object){
function onpropertychange(){
var prop = event.propertyName,
newValue = empty[prop]
prevValue = Object[prop],
handler = handlers[prop];
if(handler)
attachEvent(detachEvent()[prop] = Object[prop] = handler.call(Object, prop, prevValue, newValue));
};
function attachEvent(){empty.attachEvent("onpropertychange", onpropertychange)};
function detachEvent(){empty.detachEvent("onpropertychange", onpropertychange);return empty};
var empty = document.createElement("empty"), handlers = {};
empty.destroy = function(){
detachEvent();
empty.parentNode.removeChild(empty);
empty = handlers = null;
};
empty.watch = function(prop, handler){handlers[prop] = handler};
empty.unwatch = function(prop){delete handlers[prop]};
attachEvent();
return (document.getElementsByTagName("head")[0] || document.documentElement).appendChild(empty);
}
)
;
})(Object.prototype.watch, Object.prototype.unwatch);

A first example, based on precedent one, is this one:

var // original object
man = {},

// create its watcher
manWatcher = createWatcher(man);

manWatcher.watch("name", function(propertyName, oldValue, newValue){
return "Mr " + newValue;
});

// assign name property
manWatcher.name = "Andrea";

// retrieve original object property
alert(man.name); // Mr Andrea

Another example is based on validation, or better, a one shot assignment over the watcher:

var obj = {},
watcher = createWatcher(obj);

watcher.watch("test", function(prop, oldValue, newValue){
return oldValue === undefined ? newValue : oldValue;
});

watcher.test = "one assignment";
watcher.test = "never again";

alert(obj.test); // one assignment

Cool?

About limits


The main one is that the for in loop will not work as expected over a watcher element because it is a DOM node and it will expose every property.
Another one could be about memory leaks and/or low execution, since there are a couple of things to do during assignment.
The good part is that the watcher and the watched will have the same property value, except for those attributes that we cannot modify in a DOM node (style, for example).

Last but not least, happy new year! :D

No comments:

Post a Comment