A few weeks back I had talked about the need to watch properties of an object and be able to take action when certain values changed. The need for this arose out of wanting to build generic components that could 'attach' themselves to other objects. One example is a drop shadow - if I add a shadow behavior to an object I want the shadow to be pinned to that object so when that object moves I also want the shadow to move with it, or when the panel is hidden the shadow should hide with it - automatically without having to explicitly hook up monitoring code to the panel. For example, in my shadow plug-in I can now do something like this (where el is the element that has the shadow attached and sh is the shadow): if (!exists) // if shadow was created
el.watch("left,top,width,height,display",
function() {
if (el.is(":visible"))
$(this).shadow(opt); // redraw
else
sh.hide();
},
100, "_shadowMove");
The code now monitors several properties and if any of them change the provided function is called. So when the target object is moved or hidden or resized the watcher function is called and the shadow can be redrawn or hidden in the case of visibility going away. So if you run any of the following code:
$("#box")
.shadow()
.draggable({ handle: ".blockheader" });
// drag around the box - shadow should follow
// hide the box - shadow should disappear with box
setTimeout(function() { $("#box").hide(); }, 4000);
// show the box - shadow should come back too
setTimeout(function() { $("#box").show(); }, 8000);
This can be very handy functionality when you're dealing with objects or operations that you need to track generically and there are no native events for them. For example, with a generic shadow object that attaches itself to any another element there's no way that I know of to track whether the object has been moved or hidden either via some UI operation (like dragging) or via code. While some UI operations like jQuery.ui.draggable would allow events to fire when the mouse is moved nothing of the sort exists if you modify locations in code. Even tracking the object in drag mode this is hardly generic behavior - a generic shadow implementation can't know when dragging is hooked up.
So the watcher provides an alternative that basically gives an Observer like pattern that notifies you when something you're interested in changes.
In the watcher hookup code (in the shadow() plugin) above a check is made if the object is visible and if it is the shadow is redrawn. Otherwise the shadow is hidden. The first parameter is a list of CSS properties to be monitored followed by the function that is called. The function called receives this as the element that's been changed and receives two parameters: The array of watched objects with their current values, plus an index to the object that caused the change function to fire.
How does it work
When I wrote it about this last time I started out with a simple timer that would poll for changes at a fixed interval with setInterval(). A few folks commented that there are is a DOM API - DOMAttrmodified in Mozilla and propertychange in IE that allow notification whenever any property changes which is much more efficient and smooth than the setInterval approach I used previously. On browser that support these events (FireFox and IE basically - WebKit has the DOMAttrModified event but it doesn't appear to work) the shadow effect is instant - no 'drag behind' of the shadow. Running on a browser that doesn't support still uses setInterval() and the shadow movement is slightly delayed which looks sloppy.
There are a few additional changes to this code - it also supports monitoring multiple CSS properties now so a single object can monitor a host of CSS properties rather than one object per property which is easier to work with. For display purposes position, bounds and visibility will be common properties that are to be watched.
Here's what the new version looks like:
$.fn.watch = function (props, func, interval, id) {
/// <summary>
/// Allows you to monitor changes in a specific
/// CSS property of an element by polling the value.
/// when the value changes a function is called.
/// The function called is called in the context
/// of the selected element (ie. this)
/// </summary>
/// <param name="prop" type="String">CSS Properties to watch sep. by commas</param>
/// <param name="func" type="Function">
/// Function called when the value has changed.
/// </param>
/// <param name="interval" type="Number">
/// Optional interval for browsers that don't support DOMAttrModified or propertychange events.
/// Determines the interval used for setInterval calls.
/// </param>
/// <param name="id" type="String">A unique ID that identifies this watch instance on this element</param>
/// <returns type="jQuery" />
if (!interval)
interval = 200;
if (!id)
id = "_watcher";
return this.each(function () {
var _t = this;
var el$ = $(this);
var fnc = function () { __watcher.call(_t, id) };
var itId = null;
var data = { id: id,
props: props.split(","),
func: func,
vals: [props.split(",").length],
fnc: fnc,
origProps: props,
interval: interval
};
$.each(data.props, function (i) { data.vals[i] = el$.css(data.props[i]); });
el$.data(id, data);
hookChange(el$, id, data.fnc);
});
function hookChange(el$, id, fnc) {
el$.each(function () {
var el = $(this);
if (typeof (el.get(0).onpropertychange) == "object")
el.bind("propertychange." + id, fnc);
else if ($.browser.mozilla)
el.bind("DOMAttrModified." + id, fnc);
else
itId = setInterval(fnc, interval);
});
}
function __watcher(id) {
var el$ = $(this);
var w = el$.data(id);
if (!w) return;
var _t = this;
if (!w.func)
return;
// must unbind or else unwanted recursion may occur
el$.unwatch(id);
var changed = false;
var i = 0;
for (i; i < w.props.length; i++) {
var newVal = el$.css(w.props[i]);
if (w.vals[i] != newVal) {
w.vals[i] = newVal;
changed = true;
break;
}
}
if (changed)
w.func.call(_t, w, i);
// rebind event
hookChange(el$, id, w.fnc);
}
}
$.fn.unwatch = function (id) {
this.each(function () {
var el = $(this);
var fnc = el.data(id).fnc;
try {
if (typeof (this.onpropertychange) == "object")
el.unbind("propertychange." + id, fnc);
else if ($.browser.mozilla)
el.unbind("DOMAttrModified." + id, fnc);
else
clearInterval(id);
}
// ignore if element was already unbound
catch (e) { }
});
return this;
}
There are basically two jQuery functions - watch and unwatch.
jQuery.fn.watch(props,func,interval,id)
Starts watching an element for changes in the properties specified.
props
The CSS properties that are to be watched for changes. If any of the specified properties changes the function specified in the second parameter is fired.
func (watchData,index)
The function fired in response to a changed property. Receives this as the element changed and object that represents the watched properties and their respective values. The first parameter is passed in this structure:
{ id: itId, props: [], func: func, vals: [] };
A second parameter is the index of the changed property so data.props[i] or data.vals[i] gets the property value that has changed.
interval
The interval for setInterval() for those browsers that don't support property watching in the DOM. In milliseconds.
id
An optional id that identifies this watcher. Required only if multiple watchers might be hooked up to the same element. The default is _watcher if not specified.
jQuery.fn.unwatch(id)
Unhooks watching of the element by disconnecting the event handlers.
id
Optional watcher id that was specified in the call to watch. This value can be omitted to use the default value of _watcher.
You can also grab the latest version of the code for this plug-in as well as the shadow in the full library at:
http://www.west-wind.com:8080/svn/jquery/trunk/jQueryControls/Resources/ww.jquery.js
watcher has no other dependencies although it lives in this larger library. The shadow plug-in depends on watcher.© Rick Strahl, West Wind Technologies, 2005-2011