Contact   •   Products   •   Search

Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs

jQuery CSS Property Monitoring Plug-in updated


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 Property to watch. If not specified (null) code is called on interval</param>    
    /// <param name="func" type="Function">
    /// Function called when the value has changed.
    /// </param>    
    /// <param name="func" type="Function">
    /// optional id that identifies this watch instance. Use if
    /// if you have multiple properties you're watching.
    /// </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;

        if (typeof (this.onpropertychange) == "object")
            el.bind("propertychange." + id, fnc);
        else if ($.browser.mozilla)
            el.bind("DOMAttrModified." + id, fnc);
        else
            itId = setInterval(fnc, interval);

        var data = { id: itId,
            props: props.split(","),
            func: func,
            vals: []
        };
        $.each(data.props, function(i) { data.vals[i] = el.css(data.props[i]); });
        el.data(id, data);
    });

    function __watcher(id) {
        var el = $(this);
        var w = el.data(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) {
            var _t = this;
            w.func.call(_t, w, i)
        }
    }
}
$.fn.unwatch = function(id) {
    this.each(function() {
        var w = $(this).data(id);
        var el = $(this);
        el.removeData();

        if (typeof (this.onpropertychange) == "object")
            el.unbind("propertychange." + id, fnc);
        else if ($.browser.mozilla)
            el.unbind("DOMAttrModified." + id, fnc);
        else
            clearInterval(w.id);
    });
    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.

Make Donation
Posted in JavaScript  jQuery  


Feedback for this Post

 
# re: jQuery CSS Property Monitoring Plug-in updated
by Kevin Pirkl September 16, 2008 @ 12:38pm
Hey Rick have you looked at Live Query Plugin and Lowpro..

http://brandonaaron.net/docs/livequery/
http://github.com/danwrong/low-pro-for-jquery/
http://www.learningjquery.com/2008/05/using-low-pro-for-jquery

Live query by default can monitor DOM & .class additions and removal..

FWIW - You still utterly and completely Rule!

Cheers
# re: jQuery CSS Property Monitoring Plug-in updated
by Strelok December 10, 2008 @ 6:27am
Maaaan. You repeatedly keep coming up with cool shit! Keep it up!
# re: jQuery CSS Property Monitoring Plug-in updated
by Josh January 26, 2009 @ 11:54am
Great article. However, I think that there is a bug in the code. When you create the "vals" property of the data object, you are creating an Array of length=1, regardless of the number of props that you are watching. You should replace that line with something like:

vals: new Array(props.split(",").length)


This should create an Array with the specified size. The following $.each method should also be updated to pass in the index as a second parameter as well:

$.each(data.vals, function(v, i) { data.vals[i] = el.css(data.props[i]); });
# re: jQuery CSS Property Monitoring Plug-in updated
by Rick Strahl January 26, 2009 @ 2:32pm
@Josh - good catch, although it doesn't really matter. Javascript initializes arrays just by assignment, so actually just assigning [] (an empty array might be enough).

vals: []


and then letting the code set val[i] in iteration will create the elements dynamically. Actually I had to test that out for myself to see that this works, so thanks for pointing this out :-}.

The following demonstrates:

var ar = [];
ar[1] = "new Val";
ar[3] = "another new";
alert(ar[1] + " " + ar[4] + " " + ar[3] + " " +ar.length);  && length 4
# re: jQuery CSS Property Monitoring Plug-in updated
by Rick Strahl January 27, 2009 @ 12:34am
@Josh - btw, what you pointed out was indeed still a problem due to moving over the data array instead of the props array. I've fixed the code in the post above, so this should work correctly now.
# re: jQuery CSS Property Monitoring Plug-in updated
by Josh January 27, 2009 @ 7:27am
Thanks for the update. However, I think there may still be a minor issue with this. I'm not too familiar with jQuery, so I may be wrong. My question has to do with the $.each method, or rather the function you pass into that method. It's my understanding that the first parameter of that function will be the value itself, and the second parameter will be the index. If this is the case, you will be iterating over the "left", "top", "width", "height", and "display" values when it looks like you want to iterate over 0, 1, 2, 3, and 4. To accomplish this, you may want to specify two parameters, like the following:

$.each(data.props, function(v, i) { data.vals[i] = el.css(data.props[i]); });

Again, I may be wrong, since my experience with jQuery is _very_ limited. Also, again, thanks for the article. It has really helped with a project that I'm working on.
# re: jQuery CSS Property Monitoring Plug-in updated
by Lars March 11, 2009 @ 11:09am
Hey Rick,

Awesome plugin, I love it. It works great for me in firefox but I'm not able to get it to fire when anything changes in IE. I've tried both IE6 and IE8. Any ideas what this could be? Below is a sample of my code which attempts to use it, which works in firefox but not ie.

 $("*[id$='MenuPanel']").children().watch("height", function() {
            setTimeout("CheckPadding();", 20);
        }
        );
# re: jQuery CSS Property Monitoring Plug-in updated
by Dean Smith October 10, 2009 @ 8:36am
I can't get this to work at all for me when using Firefox, I've tried changing the "DOMAttrModified." references to not include the . but that doesn't work. The only way I can get it to work is to remove the checks for Firefox but then it's really slow.
# re: jQuery CSS Property Monitoring Plug-in updated
by Herb Caudill January 22, 2010 @ 12:59pm
This stopped working after I upgraded to jQuery 1.4. It looks like the onpropertychange event was getting triggered before the data had been added to the event - maybe just a matter of 1.4 being faster? Anyway I was able to get it working it by just changing the order so that we add the data to the element *before* wiring up the events. Here's what's working for me right now - haven't tested this with a browser that doesn't support propertychange etc.

    return this.each(function() {
        var _t = this;
        var el = $(this);
        var fnc = function() { __watcher.call(_t, id) };
        var itId = null;
        var data = { props: props.split(","),
            func: func,
            vals: [props.split(",").length]
        };
        $.each(data.vals, function(i) { data.vals[i] = el.css(data.props[i]); });
        el.data(id, data);
        if (typeof (this.onpropertychange) == "object")
            el.bind("propertychange." + id, fnc);
        else if ($.browser.mozilla)
            el.bind("DOMAttrModified." + id, fnc);
        else {
            itId = setInterval(fnc, interval);
            el.data(id).id = itld;
        }
    });
# re: jQuery CSS Property Monitoring Plug-in updated
by Darcy Clarke October 15, 2010 @ 10:21am
I've made a similar plugin to Rick's here:

Post: http://darcyclarke.me/development/detect-attribute-changes-with-jquery/
Demo: http://darcyclarke.me/dev/watch/

It's a bit more flexible and the blog post explains a bit about the problem I was having and how I came to this solution (along with a reference to this Rick's initial plugin here as I found it along my way)
# re: jQuery CSS Property Monitoring Plug-in updated
by Silah March 11, 2011 @ 12:52pm
@Josh - good catch, although it doesn't really matter. Javascript initializes arrays just by assignment, so actually just assigning [] (an empty array might be enough).
# re: jQuery CSS Property Monitoring Plug-in updated
by brainTrain December 01, 2011 @ 3:34pm
dude you saved me so much god damned work it's awesome. I'm using this to (hopefully) make a super awesome bookmarklet that'll help increase the effectiveness of developer tools in chrome (to start) and probably(hopefully) firebug and beyond.

your comments at the top of your plug in don't actually give any info on you/your site, so I added this(sofar):


/// source (site or increase your awful level by over 5,000):
/// http://www.west-wind.com/weblog/posts/2008/Sep/12/jQuery-CSS-Property-Monitoring-Plugin-updated

Do you want me to put any additional or different info in there?

if you're interested:
my bookmarklet logs diffs in css (thanks to you) and logs them to loggly. Doing this because at the moment my CSS development flow is as follows:
-create html elements
-set up basic guesses, etc, for css styles
-open up page in chrome and fiddle with css in developer tools until it looks how I want

problem I'm trying to solve with this bookmarklet:
that annoying thing that happens when you accidentally click on a link/refresh the page and lose ALL your developer tools CSS changes. If the diffs are logged, you've got a safety net (and change history) to help dumb mistakes from holding your front end deving back.

anyway, thanks for the rad function!

-brainTrain
# re: jQuery CSS Property Monitoring Plug-in updated
by kadın February 03, 2012 @ 7:22am
Maaaan. You repeatedly keep coming up with cool shit! Keep it up!..
# re: jQuery CSS Property Monitoring Plug-in updated
by kayahr February 27, 2012 @ 11:29pm
Is this plugin still maintained and works with the latest jQuery 1.7.1? I can't fetch it from your SVN repo because a login window pops up when I click the link.
# re: jQuery CSS Property Monitoring Plug-in updated
by Rick Strahl February 28, 2012 @ 12:07am
kayahr - yes it still works - I use it with various apps using 1.7.1. Oops yes the link is bad. The old jQuery repository has moved to the West Wind Web Toolkit site. The new Url is:

http://www.west-wind.com:8080/svn/WestwindWebToolkit/trunk/Westwind.Web/Resources/ww.jquery.js

I'll update the post.
 


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2014