A jQuery Plug-in to monitor Html Element CSS Changes
Here's a scenario I've run into on a few occasions: I need to be able to monitor certain CSS properties on an HTML element and know when that CSS element changes. The need for this arose out of wanting to build generic components that could 'attach' themselves to other objects and monitor changes on the ‘parent’ object so the dependent object can adjust itself accordingly.
What I wanted to create is a jQuery plug-in that allows me to specify a list of CSS properties to monitor and have a function fire in response to any change to any of those CSS properties. The result are the .watch() and .unwatch() jQuery plug-ins. Here’s a simple example page of this plug-in that demonstrates tracking changes to an element being moved with draggable and closable behavior:
http://www.west-wind.com/WestWindWebToolkit/samples/Ajax/jQueryPluginSamples/WatcherPlugin.htm
Try it with different browsers – IE and FireFox use the DOM event handlers and Chrome, Safari and Opera use setInterval handlers to manage this behavior. It should work in all of them but all but IE and FireFox will show a bit of lag between the changes in the main element and the shadow.
The relevant HTML for this example is this fragment of a main <div> (#notebox) and an element that is to mimic a shadow (#shadow).
<div class="containercontent"> <div id="notebox" style="width: 200px; height: 150px;position: absolute;
z-index: 20; padding: 20px; background-color: lightsteelblue;"> Go ahead drag me around and close me! </div> <div id="shadow" style="background-color: Gray; z-index: 19;position:absolute;display: none;"> </div></div>
The watcher plug in is then applied to the main <div> which keeps the shadow in sync with the main element. The following plug-in code demonstrates:
<script type="text/javascript"> $(document).ready(function () { var counter = 0; $("#notebox").watch("top,left,height,width,display,opacity", function (data, i) { var el = $(this); var sh = $("#shadow"); var propChanged = data.props[i]; var valChanged = data.vals[i]; counter++; showStatus("Prop: " + propChanged + " value: " + valChanged + " " + counter); var pos = el.position(); var w = el.outerWidth(); var h = el.outerHeight(); sh.css({ width: w, height: h, left: pos.left + 5, top: pos.top + 5, display: el.css("display"), opacity: el.css("opacity") }); }) .draggable() .closable() .css("left", 10); });</script>
When you run this page as you drag the #notebox element the #shadow element will maintain and stay pinned underneath the #notebox element effectively keeping the shadow attached to the main element. Likewise, if you hide or fadeOut() the #notebox element the shadow will also go away – show the #notebox element and the shadow also re-appears because we are assigning the display property from the parent on the shadow.
Note we’re attaching the .watch() plug-in to the #notebox element and have it fire whenever top,left,height,width,opacity or display CSS properties are changed. The passed data element contains a props[] and vals[] array that holds the properties monitored and their current values. An index passed as the second parm tells you which property has changed and what its current value is (propChanged/valChanged in the code above). The rest of the watcher handler code then deals with figuring out the main element’s position and recalculating and setting the shadow’s position using the jQuery .css() function.
Note that this is just an example to demonstrate the watch() behavior here – this is not the best way to create a shadow. If you’re interested in a more efficient and cleaner way to handle shadows with a plug-in check out the .shadow() plug-in in ww.jquery.js (code search for fn.shadow) which uses native CSS features when available but falls back to a tracked shadow element on browsers that don’t support it, which is how this watch() plug-in came about in the first place :-)
How does it work?
The plug-in works by letting the user specify a list of properties to monitor as a comma delimited string and a handler function:
el.watch("top,left,height,width,display,opacity", function (data, i) {}, 100, id)
You can also specify an interval (if no DOM event monitoring isn’t available in the browser) and an ID that identifies the event handler uniquely.
The watch plug-in works by hooking up to DOMAttrModified in FireFox, to onPropertyChanged in Internet Explorer, or by using a timer with setInterval to handle the detection of changes for other browsers. Unfortunately WebKit doesn’t support DOMAttrModified consistently at the moment so Safari and Chrome currently have to use the slower setInterval mechanism. In response to a changed property (or a setInterval timer hit) a JavaScript handler is fired which then runs through all the properties monitored and determines if and which one has changed. The DOM events fire on all property/style changes so the intermediate plug-in handler filters only those hits we’re interested in. If one of our monitored properties has changed the specified event handler function is called along with a data object and an index that identifies the property that’s changed in the data.props/data.vals arrays.
The jQuery plugin to implement this functionality looks like this:
(function($){$.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 = 100; if (!id) id = "_watcher"; return this.each(function () { var _t = this; var el$ = $(this); var fnc = function () { __watcher.call(_t, id) }; var data = { id: id, props: props.split(","), vals: [props.split(",").length], func: func, fnc: fnc, origProps: props, interval: interval, intervalId: null }; // store initial props and values $.each(data.props, function (i) { data.vals[i] = el$.css(data.props[i]); }); el$.data(id, data); hookChange(el$, id, data); }); function hookChange(el$, id, data) { el$.each(function () { var el = $(this); if (typeof (el.get(0).onpropertychange) == "object") el.bind("propertychange." + id, data.fnc); else if ($.browser.mozilla) el.bind("DOMAttrModified." + id, data.fnc); else data.intervalId = setInterval(data.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); }}$.fn.unwatch = function (id) { this.each(function () { var el = $(this); var data = el.data(id); try { if (typeof (this.onpropertychange) == "object") el.unbind("propertychange." + id, data.fnc); else if ($.browser.mozilla) el.unbind("DOMAttrModified." + id, data.fnc); else clearInterval(data.intervalId); } // ignore if element was already unbound catch (e) { } }); return this;}
})(jQuery);
Note that there’s a corresponding .unwatch() plug-in that can be used to stop monitoring properties. The ID parameter is optional both on watch() and unwatch() – a standard name is used if you don’t specify one, but it’s a good idea to use unique names for each element watched to avoid overlap in event ids especially if you’re monitoring many elements.
The syntax is:
$.fn.watch = function(props, func, interval, id)
props
A comma delimited list of CSS style 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
The function fired in response to a changed styles. Receives this as the element changed and an object parameter that represents the watched properties and their respective values. The first parameter is passed in this structure:
{ id: watcherId, props: [], vals: [], func: thisFunc, fnc: internalHandler, origProps: strPropertyListOnWatcher };
A second parameter is the index of the changed property so data.props[i] or data.vals[i] gets the property and changed value.
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.
It’s been a Journey
I started building this plug-in about two years ago and had to make many modifications to it in response to changes in jQuery and also in browser behaviors. I think the latest round of changes made should make this plug-in fairly future proof going forward (although I hope there will be better cross-browser change event notifications in the future).
One of the big problems I ran into had to do with recursive change notifications – it looks like starting with jQuery 1.44 and later, jQuery internally modifies element properties on some calls to some .css() property retrievals and things like outerHeight/Width(). In IE this would cause nasty lock up issues at times. In response to this I changed the code to unbind the events when the handler function is called and then rebind when it exits. This also makes user code less prone to stack overflow recursion as you can actually change properties on the base element. It also means though that if you change one of the monitors properties in the handler the watch() handler won’t fire in response – you need to resort to a setTimeout() call instead to force the code to run outside of the handler:
$("#notebox")el.watch("top,left,height,width,display,opacity", function (data, i) { var el = $(this);
… // this makes el changes work setTimeout(function () { el.css("top", 10) },10);})
Since I’ve built this component I’ve had a lot of good uses for it. The .shadow() fallback functionality is one of them.
Resources
The watch() plug-in is part of ww.jquery.js and the West Wind West Wind Web Toolkit. You’re free to use this code here or the code from the toolkit.
- West Wind Web Toolkit
- Latest version of ww.jquery.js (search for fn.watch)
- watch plug-in documentation
Other Posts you might also like
The Voices of Reason
# re: Monitoring Html Element CSS Changes in JavaScript
# re: Monitoring Html Element CSS Changes in JavaScript
If I were to do this manually each time for dragging I get events that can be handled for movement that give me position as the element is dragged. But this required explicit hookup of events and it wouldn't be generic.
# re: Monitoring Html Element CSS Changes in JavaScript
(function ($) { $.fn.watch = function (property, id, fn) { id += "Watcher"; return this .data(id, {val: null, fn: watcher(property, id)}) .bind("DOMAttrModified", this.data(id).fn); }; $.fn.unwatch = function (id) { id += "Watcher"; var data = this.data(id); if(!data) return this; return this .unbind("DOMAttrModified", data.fn) .removeData(id); }; function watcher (property, id) { return function (event) { // First statement "filters" events from child nodes if(this !== event.originalTarget || event.attrName !== "style") return false; var $this = $(this), new_val = $this.css(property), data = $this.data(id); if(!data) return false; if(new_val !== data.val) return data.fn.call(this, data.val = new_val); } } })(jQuery) // Example: $(".scrollbar").watch("top", "offset", function (top) { console.log(this, top) }); $(".scrollbar").unwatch("offset");
The (only) downside of this method is that "DOMAttrModifed" event isn't supported by all browsers.
# re: Monitoring Html Element CSS Changes in JavaScript
Any good draggable implementation should be publishing events for you to subscribe to.
For a shadow, why don't you position the shadow relative to the draggable in CSS. That way it will just follow along? Or have the shadow be part of the draggable itself?
# re: Monitoring Html Element CSS Changes in JavaScript
One offs where you can hook events and you have full control is no problem. A bit of work and tedious, but doable. It's to this generically when it gets ugly.
Hmmm... relative positioning relative to the object - how would you do that unless you have a wrapper element (which as discussed above isn't really doable generically).
# re: Monitoring Html Element CSS Changes in JavaScript
Is there something I am missing? Or is it that you are just trying to use the plugins you have out of the box with minimal custom coding/modifications?
# re: Monitoring Html Element CSS Changes in JavaScript
I'm not sure if it would quite work in this case, but it re-executes your jquery statement when the DOM changes from at least an AJAX query, if not other dom changing events.
# re: Monitoring Html Element CSS Changes in JavaScript
Bummer, but that would definitely solve the problem nicely.
# re: Monitoring Html Element CSS Changes in JavaScript
you have:
/// <param name="func" type="Function">
needs to be:
/// <param name="id" type="String">
# re: Monitoring Html Element CSS Changes in JavaScript
First; thanks for posting this code; it gave me a great place to start when I needed to solve a similar problem. I've used your code above as a base and extended it in several ways.
- "prop" can now be sent in as an array (actual or string). This lets you monitor several properties under a single id.
- "interval" was moved to the 4th position. Since more browsers now support the event natively than they did before, there should be less need to define the interval compared to the "id". It could easily be switch if you need backword compatability.
- The "data" object sent to the callback now uses an array and index for the "props" and "vals" properties. Also added in a "prevVals" property that will contain what the value was before the event fired. For example:
$( 'input' ).watch( ['display','value'], function( data, i ) { alert( data.props[ i ] + ' changed from ' + data.prevVals[ i ] + ' to ' + data.vals[ i ] ); });
- $.fn.watchPause( prop, id ) and $.fn.watchResume( prop, id ) have been added. These two functions allow you to pause and resume a previously bound watcher. Among other things, these functions can also be used to "cancel" a property change using the "prevVals" property:
$( 'div' ).watch( 'width', function( data, i ) { // Prevent the width from changing $( this ) .watchPause() .width( data.prevVals[ i ] ) .watchResume(); });
It's not gone through much testing yet, but hopefully it'll hold up... ;-)
(function( $, undefined ) { $.fn.watch = function(props, func, id, interval) { /// <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.</param> /// <param name="func" type="Function">Function called when the value has changed.</param> /// <param name="id" type="String">A unique ID that identifies this watch instance on this element</param> /// <param name="interval" type="Int">A unique ID that identifies this watch instance on this element</param> /// <returns type="jQuery" /> if ( !$.isFunction( func ) || ! props ) { return this; } if ( !id ) { id = '_watcher'; } if ( typeof props == 'string' ) { props = props.split( ',' ); } if ( !$.isArray( props ) ) { return this; } if ( !interval ) { interval = 200; } function __watcher( id, i ) { var el = $(this), changed = false, w = $.data( this, id ), newVal = el.css( w.props[i] ); if (w.vals[i] != newVal) { w.prevVals[i] = w.vals[i]; w.vals[i] = newVal; if ( w.pause == false || ( w.pause != true && $.inArray( w.props[i], w.pause ) == -1 ) ) { w.func.call( this, w, i ); } } } return this.each(function() { var _t = this, el = $( this ), fnc = function() { var _prop = window.event.attrChange ? window.event.attrName : window.event.propertyName; if ( _prop != undefined ) { _prop = _prop.indexOf( '.' ) > -1 ? _prop.split( '.' )[ 1 ] : _prop; var w = $.data( _t, id ); var index = $.inArray( _prop, w.props ); if ( index > -1 ) { __watcher.call( _t, id, index ); } } }, data = { props: props, func: func, vals: [], prevVals: [], _fnc: fnc, interval: interval, itId: false, pause: false }; $.each( props, function(i) { data.prevVals[ i ] = data.vals[ i ] = el.css( props[ i ] ); }); if ( typeof( this.onpropertychange ) == 'object' ) { el.bind( 'propertychange.' + id, fnc); } else if ( $.browser.mozilla ) { el.bind( 'DOMAttrModified.' + id, fnc); } else { data.itId = setInterval( fnc, interval ); } $.data( this, id, data ); }); }; $.fn.unwatch = function( props, id ) { if ( !id ) { id = '_watcher'; } if ( props ) { if ( typeof props == 'string' ) { props = props.split( ',' ); } if ( !$.isArray( props ) ) { props = false; } } return this.each(function() { var el = $( this ), w = $.data( this, id ); if ( $.isPlainObject( w ) && w.props ) { if ( props ) { w.props = $.grep( w.props, function( _prop ) { return $.inArray( _prop, props ) == -1; }); } if ( w.props.length == 0 || !props ) { if ( typeof( this.onpropertychange ) == 'object' ) { el.unbind( 'propertychange.' + id ); } else if ($.browser.mozilla) { el.unbind( 'DOMAttrModified.' + id ); } else { clearInterval( w.itId ); } $.removeData( this, id ); } else { $.data( this, id, w ); } } }); }; $.fn.watchPause = function( props, id ) { if ( !id ) { id = '_watcher'; } if ( props ) { if ( typeof props == 'string' ) { props = props.split( ',' ); } if ( !$.isArray( props ) ) { props = true; } } else { props = true; } return this.each(function() { var w = $.data( this, id ); if ( $.isPlainObject( w ) ) { if ( props != true ) { var p = w.pause || []; w.pause = p.concat( props ); } else { w.pause = true; if ( w.itId ) { clearInterval( w.itId ); w.itId = true; } } $.data( this, id, w ); } }); }; $.fn.watchResume = function( props, id ) { if ( !id ) { id = '_watcher'; } if ( props ) { if ( typeof props == 'string' ) { props = props.split( ',' ); } if ( !$.isArray( props ) ) { props = false; } } else { props = false; } return this.each(function() { var w = $.data( this, id ); if ( $.isPlainObject( w ) ) { if ( props != false ) { var p = w.pause; if ( p != true ) { p = $.grep( p, function( _prop ) { return $.inArray( _prop, props ) == -1; }); if ( p.length == 0 ) { props = false; } } } w.pause = props; if ( w.itId == true ) { w.itId = setInterval( w._fnc, w.interval ); } $.data( this, id, w ); } }); }; }( jQuery ));
Thanks again!
- Dave
# re: Monitoring Html Element CSS Changes in JavaScript
The kind of problem you run into seems to be similar to the one I exposed on Stack Overflow a while ago.
Basically there is a plugin called Livequery, which was created by a former jQuery Core Team member : Brandon Aaron.
This plugin works as if it implemented the Observable pattern.
More on this here if you want a basic illustration:
http://stackoverflow.com/questions/3367698/jquery-capturing-an-event-the-current-element-has-a-specific-class-but-change
Hope this help :p
Roland
# re: Monitoring Html Element CSS Changes in JavaScript
# re: Monitoring Html Element CSS Changes in JavaScript
How long did it take you to write the plugin?
# re: Monitoring Html Element CSS Changes in JavaScript
# re: A jQuery Plug-in to monitor Html Element CSS Changes
It is very helpful, I use it for equal height column layouts to catch any change in the height of adjacent columns to resize them again and works so great with only 1 problem and it is IE, both 7 and 8, the page loads very slow when I retrieve all children and sub children of a selector, the code I have looks like this:
jQuery(document).ready( function() {
jQuery('#Col1 ,#Col2 ,#col3').find('div, dd, dt, ul, ol, td').watch('display, height', function(){
jQuery('#Col1 ,#Col2 ,#col3').removeAttr('style');
jQuery('#Col1 ,#Col2 ,#col3').matchHeights();
});
It is very very fast in all other browsers like Safari, Chrome, FF and Opera except as normal in IE... I wonder if there is a solution to speed it up in IE? I will be appreciative for your kind answer.
Best regards
/Hussain
# re: A jQuery Plug-in to monitor Html Element CSS Changes
I discovered another issue, I have a form and use a jQuery plugin to validate it, the validator inserts label tags as error message and I have label tags in my style sheet declared as block, means display:block and this makes the height of my form wrapper DIV to grow but the watch plugin does not catch the change in the height... Again the code looks like this:
jQuery('#JMRightColInner ,#JMCenterColInner ,#JMLeftColInner, .pane-slider').find('*').watch('display, height', function(){
jQuery('#JMRightColInner ,#JMCenterColInner ,#JMLeftColInner').removeAttr('style');
jQuery('#JMRightColInner ,#JMCenterColInner ,#JMLeftColInner').equalHeightColumns();
});
But this time I retrieve all children and sub-children of my root elements. I use both mootools and jquery libs in my design and have noticed watch plugin catches modifications done by mootools but not those done by jQuery as in example.... Have any idea why it is like that dear Rick?
Many thanks in advance
/Hussain
# re: A jQuery Plug-in to monitor Html Element CSS Changes
While your proposed changes sound interesting, your implementation fails for me. It keeps returning "TypeError: Result of expression 'window.event' [undefined] is not an object. (line 49)" every x milliseconds. This is on Safari 5.
# re: A jQuery Plug-in to monitor Html Element CSS Changes
http://stackoverflow.com/questions/4562354/javascript-detect-if-event-lister-is-supported
# re: Monitoring Html Element CSS Changes in JavaScript
Could work