My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

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

8 comments:

Unknown said...

Interesting! I wonder why the wrapping function passing indexOf, since it's not used in the closure.

Shame the added DOM nodes won't get cleaned up when the watched object is GCd. Could lead to some serious memory leakage if used in a loop or high traffic util function.

I suppose neither Opera nor Safari have any comparable path?

Andrea Giammarchi said...

Luke, the indexOf was for another test I did but I forgot to remove it from the code, thanks.

I could not find a way to automatically clean everything on watcher delete and any sugestion will be appreciated.

Opera and Safari, as far as I know, supports __defineSetter__ and __defineGetter__, am I wrong? I could extend the function to those browsers as well ( right now I think Opera could work without problems, but I did not test :) )

Andrea Giammarchi said...

Luke, I implemented a destroy method to remove the element and/or callbacks and solve memory leaks problem plus I have implemented an Opera / Safari version.

The script is beta but seems to work properly ;-)

Anonymous said...

Could you please explain this "watcher" concept again. Also, is your implementation cross-browser compat?

Andrea Giammarchi said...

Anonymous, this is the MDC explanation of the watch Object prototype: Object watch

while for the other question, yes, the last version is compatible with Opera and Safari and probably others.

Anonymous said...

doesn't work at all
no errors but no result

function myObj()
{
this.towatch=false;
manWatcher=createWatcher(this);
manWatcher.watch("towatch",function(){alert("ok");});
}

var z=new myObj();
z.towatch=true;

Mattmo said...

Hi Andrea,

I'm trying to find a good way to implement an object.watch functionality. The event chain in my application relies on it. I tried the Eli Grey polyfill, but it gave me some issues witch the variables becoming inaccessible. I wondered what your thought at the present time are about the watch function. Do you think your function is still considered a good practice?

wkr,

Jerry

Andrea Giammarchi said...

I don't think this post is updated enough so I think it's not a solution at all ... not even sure if still works.

However, what you are looking for is `Object.observe` in Chrome or `Proxy`, both should land via ES6 soon as JavaScript core functionality.

I hope this answer helped.