Collecting DOM events

The DOM change notification I presented yesterday is one piece in the puzzle to rejuvenate my languishing JavaScript form validator. I’ve not yet made up my mind on a lot of issues, but there’s one thing that’s certain: the next version will have to be able to work with changing forms, that is, forms that show different input elements depending on already chosen values.

Since yesterday, I have a way to be notified when the DOM has changed. As it is, the dom:changed custom event is completely general and not quite what I need. Of course, I do need some kind of notification, but what I get when a function changes the DOM in several places, is a barrage of dom:changed events. I don’t want to do lots of stuff for each of these events, but I’ve got to do something about the events collectively. There is no natural bracketing for events, there’s nothing that says which events belong to the same high-level change. But that’s not really necessary, rather, it is good enough to collect all relevant events that occur in period of time.

Here’s a class that collects all events that it receives over a given interval (of seconds) and passes them on to a handler function.

Element.EventCollector = Class.create({
  initialize: function(handler, interval) {
    this.handler = handler;
    this.interval = (interval || 1) * 1000;
    this.reset();
    this.dischargeEvents = this.discharge.bind(this);
  },
  reset: function() {
    this.events = [];
    this.timer = null;
  },
  observer: function() {
    return this.onEvent.bindAsEventListener(this);
  },
  onEvent: function(event) {
    this.events.push(event);
    this.wait();
  },
  wait: function() {
    if (!this.timer) {
      this.timer = setTimeout(this.dischargeEvents, this.interval);
    }
  },
  discharge: function() {
    this.timer = null;
    var events = this.events;
    this.reset();
    this.handler(events);
  }
});

Add some syntactic sugar, so that we can do things like

document.body.collectEvents('dom:changed', function(events) { ... });

Element.addMethods({
  collectEvents: function(element, eventName, handler, interval) {
    var collector = new Element.EventCollector(handler, interval);
    $(element).observe(eventName, collector.observer());
  }
});

For my motivating case of the validator that’s still not exactly what I need. I can arrange to get a notification that a whole bunch of elements was changed, but I don’t want to deal with these elements individually. The validator I have (and the next version I envision) works it’s way down from a starting element and attaches its magic pixie dust to everything below. Thus, what I need is a common ancestor element for all the changed elements. The idea is easy: Go down from the root of the “family” (document) tree and follow it until the path to the elements branches.

Element.addMethods({
  commonAncestor: function() {
    var ancestorsLists = $A(arguments).map(function(el) {
      el = $(el);
      var ancestors = el.ancestors().reverse()
      ancestors.push(el);
      return ancestors;
    });
    var common = document;
    Enumerable.zip.apply(ancestorsLists.shift(), ancestorsLists).each(
      function(ancestorTuple) {
        var cand = ancestorTuple.shift();
        if (!ancestorTuple.all(function(el) { return el == cand; })) {
          throw $break;
        }
        common = cand;
      });
    return common;
  }
});

[See Element#ancestors, Enumerable#zip, $break.]

Finally, here’s an example that puts the existing pieces together.

document.observe('dom:loaded', function() {
  document.body.collectEvents('dom:changed', function(events) {
    var affectedElements = events.invoke('element');
    var ancestor = Element.commonAncestor.apply(null, affectedElements);
    console.log('dom:changed below: ', ancestor);
  }, 10);
});