Attribute Change Detection for SharePoint 2010 Row Selection

SharePoint 2010 has some quirky architecture to handle row selection in list views. We’re all familiar with this from navigating around lists. In the list views, we have a few choices on how we select rows to work with (depending on the list view settings):

  • Clicking the checkbox to the left of each item
  • Clicking on the row itself to highlight and select/deselect it
  • Clicking on the checkbox in the title row to toggle ”’Select or deselect all items’

List View Row SelectionThe are times when we’d like to be able to intercept those actions so that we can manage other content in the page. It’s not so simple if you’d like it to work across browsers.

In this example, we were building a performance review capability into My Sites. Each person would set goals by adding items to the Goals list and then they and others could comment on those goals in a Comments list. It was more complicated than this, but you get the idea.

One of the complexities was that we didn’t want to allow comments on draft goals, and there were several other limiting circumstances as well. Suffice it to say that we needed to catch any of the events above so that we could count up various characteristics of the selected goals and change the behavior of other list views and links on the page. Trust me when I say that simple old Web Part Connections didn’t hack it; we needed some custom script to tie it all together.

For this post, please consider all of the code snippets examples, as your environment may well vary. (I shouldn’t need to say this, but I get a lot of “this didn’t work for me when I copy/pasted it into my page” comments.)

Getting this to work in Internet Explorer was relatively simple.

// You'd use your unique Web Part id here
var goalsWP = $("#WebPartWPQ2");
var goalCheckboxes = $("input.s4-itm-cbx");
var goalRows = $("tr.ms-itmhover");
var goalSelectAll = goalsWP.find("input[title='Select or deselect all items']");

// When the user checks a goal's checkbox...
goalCheckboxes.change(function() {
  countSelected();
});
// ...or clicks on the goal's row to select...
goalRows.click(function() {
  countSelected();
});
// ...or clicks the 'Select or deselect all items' checkbox...
goalSelectAll.change(function() {
  countSelected();
});

The countSelected() function does all the stuff we need, but the contents of the function don’t really matter for this post.

The problem came when we started looking at things in Firefox and Safari on PCs and Safari on Macs. All of the browsers have slightly different behavior when it comes to bubbling up events. We went through several iterations on this which would fix things in one browser while breaking them in another.

When I looked to the Web for some hints, I found a great post from Darcy Clarke. What we needed was very similar to what Darcy has done in his .watch() jQuery function. In his case, he wanted to detect CSS changes to elements, perhaps the background color changing. In our case, it was more than just a CSS change, but his code formed a good basis. Someone named Frank posted some tweaks to Darcy’s original function that made it work with any element attributes, not just CSS changes.

This is an area where the browsers are fairly different, unfortunately. IE has its unique propertychange event, where the other browsers have their quirks as well, though DOMAttrModified is sometimes supported. The article points out that DOMAttrModified is not universally supported yet, though – perhaps not yet in Chrome and Safari – though the article is a bit dated. The fallback is to attach a check to the element using setInterval. This latter approach is expensive, so it should be the last resort.

In the case of the row selection in SharePoint list views, we can rely on the fact that each tr’s class will change from “ms-itmhover” to “ms-itmhover s4-itm-selected” when the row is selected. All this comes down to the odd way Microsoft has set up the selection behavior using a row click and highlight, which is using table rows rather than divs.

What I ended up with is actually pretty good, and I expect I’ll use it again. This isn’t the first time I’ve wanted to watch a DOM element for *any* change, and not just an event-triggering one. It’s an adapted version of Darcy’s .watch() function, with Frank’s additions, tweaked to use feature detection rather than the deprecated (removed in modern versions) of jQuery’s $.browser function. On top of that, to prevent a ridiculous number of calls to countSelected(), which is doing some reasonably heavy lifting, I added an additional check to make absolutely sure that the class has changed by saving the current class into data on the row element and checking it on each firing. It doesn’t seem as though this should be necessary, but I was seeing .watch() fire quite a few more times that I needed.

Here’s the resulting code to detect the different types of user actions:

// When the user checks a goal's checkbox...
goalCheckboxes.change(function() {
  countSelected();
});
// ...or clicks on the goal's row to select...
goalRows.watch("class", function() {
  // Make sure the class has actually changed
  if($(this).data("class") !== $(this).attr("class")) {
    $(this).data("class", $(this).attr("class"));
    countSelected();
  }
});
// ...or clicks the 'Select or deselect all items' checkbox...
goalSelectAll.change(function() {
  countSelected();
});

And the revised .watch() function:

$.fn.watch = function (props, callback, timeout) {
  if (!timeout)
    timeout = 10;
  return this.each(function () {
    var el = $(this),
    func = function () {
      __check.call(this, el)
    },
    data = {
      props : props.split(","),
      func : callback,
      vals : []
    };
    $.each(data.props, function (i) {
      data.vals[i] = (!el.css(data.props[i]) ? el.attr(data.props[i]) : el.css(data.props[i]));
    });
    el.data(data);
    if (typeof(this.onpropertychange) == "object") {
      el.bind("propertychange", callback);
    } else if (this.DOMAttrModified) {
      el.bind("DOMAttrModified", callback);
    } else {
      setInterval(func, timeout);
    }
  });
  function __check(el) {
    var data = el.data(),
    changed = false,
    temp = "";
    for (var i = 0; i < data.props.length; i++) {
      temp = (!el.css(data.props[i]) ? el.attr(data.props[i]) : el.css(data.props[i]));
      if (data.vals[i] != temp) {
        data.vals[i] = temp;
        changed = true;
        break;
      }
    }
    if (changed && data.func) {
      data.func.call(el, data);
    }
  }
}

Similar Posts

5 Comments

  1. Marc,
    This didn’t work for me when I copy/pasted it into my page – can you help?

    :) Kidding.

    Thanks for posting and sharing the re-usable method above.

    I too have something similar that wraps jQuery’s Deferred (can you tell I love deferreds?)… I made my function (called .doWhen() ) completely generic… The function has two important input params: “when” and “exec” – both set to functions.

    “exec” param – This is a function that is executed when the condition I’m checking for returns true.

    “when” param – This is a function which is used to check on anything I want – when I created it, I wanted to do a check on when elements were added to a specific DIV… It is executed using a setInterval()… This function MUST return true (boolean) in order for the “exec” input parameter function to execute.

    I have other input params, like interval setting and max tries as well – allowing fine tuning… and because I have doWhen() return a Promise, I can “attach” on other methods to execute when it is resolved. As it stands today, my function does not run indefinitely – once it either reaches the max number of attempts or the “when” function returns true, it will destroy the setInterval and stop looping.

    I been meaning to do a post on it and paste the code to a gist… one of these days.

    Paul.

    1. Paul:

      That sounds useful, too. BTW, I’m totally enamored of promises now, too. Especially when you’re working in IE (which we are the majority of the time with SharePoint) they make a *huge* difference because IE locks everything up – animated GIFs, even – during a synchronous AJAX call.

      M.

  2. Thanks Marc, I’ve been looking for this for month’s, just didn’t find the magically Google words before. This is going to save me sooooooo much time!!!! Nice and simple and sweet! Thanks again.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.