{"id":37,"date":"2008-01-08T21:24:12","date_gmt":"2008-01-08T20:24:12","guid":{"rendered":"http:\/\/schuerig.de\/michael\/blog\/index.php\/2008\/01\/08\/event-collector\/"},"modified":"2021-11-24T09:20:32","modified_gmt":"2021-11-24T08:20:32","slug":"event-collector","status":"publish","type":"post","link":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/2008\/01\/08\/event-collector\/","title":{"rendered":"Collecting DOM events"},"content":{"rendered":"<p>The DOM change notification I presented <a href=\"http:\/\/schuerig.de\/michael\/blog\/index.php\/2008\/01\/07\/dom-change-notification\/\">yesterday<\/a> is one piece in the puzzle to rejuvenate my languishing JavaScript <a href=\"http:\/\/schuerig.de\/michael\/blog\/index.php\/2006\/12\/15\/rails-almost-automatic-client-side-validation\/\">form validator<\/a>. I&#8217;ve not yet made up my mind on a lot of issues, but there&#8217;s one thing that&#8217;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.<\/p>\n<p>Since yesterday, I have a way to be notified when the DOM has changed. As it is, the <tt>dom:changed<\/tt> 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 <tt>dom:changed<\/tt> events. I don&#8217;t want to do lots of stuff for each of these events, but I&#8217;ve got to do something about the events collectively. There is no natural bracketing for events, there&#8217;s nothing that says which events belong to the same high-level change. But that&#8217;s not really necessary, rather, it is good enough to collect all relevant events that occur in period of time.<\/p>\n<p>Here&#8217;s a class that collects all events that it receives over a given <tt>interval<\/tt> (of seconds) and passes them on to a <tt>handler<\/tt> function.<\/p>\n<p><code><\/p>\n<pre>\r\nElement.EventCollector = Class.create({\r\n  initialize: function(handler, interval) {\r\n    this.handler = handler;\r\n    this.interval = (interval || 1) * 1000;\r\n    this.reset();\r\n    this.dischargeEvents = this.discharge.bind(this);\r\n  },\r\n  reset: function() {\r\n    this.events = [];\r\n    this.timer = null;\r\n  },\r\n  observer: function() {\r\n    return this.onEvent.bindAsEventListener(this);\r\n  },\r\n  onEvent: function(event) {\r\n    this.events.push(event);\r\n    this.wait();\r\n  },\r\n  wait: function() {\r\n    if (!this.timer) {\r\n      this.timer = setTimeout(this.dischargeEvents, this.interval);\r\n    }\r\n  },\r\n  discharge: function() {\r\n    this.timer = null;\r\n    var events = this.events;\r\n    this.reset();\r\n    this.handler(events);\r\n  }\r\n});\r\n<\/pre>\n<p><\/code><\/p>\n<p>Add some syntactic sugar, so that we can do things like<\/p>\n<p><code><\/p>\n<pre>\r\ndocument.body.collectEvents('dom:changed', function(events) { ... });\r\n<\/pre>\n<p><\/code><\/p>\n<p><code><\/p>\n<pre>\r\nElement.addMethods({\r\n  collectEvents: function(element, eventName, handler, interval) {\r\n    var collector = new Element.EventCollector(handler, interval);\r\n    $(element).observe(eventName, collector.observer());\r\n  }\r\n});\r\n<\/pre>\n<p><\/code><\/p>\n<p>For my motivating case of the validator that&#8217;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&#8217;t want to deal with these elements individually. The validator I have (and the next version I envision) works it&#8217;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 &#8220;family&#8221; (document) tree and follow it until the path to the elements branches.<\/p>\n<p><code><\/p>\n<pre>\r\nElement.addMethods({\r\n  commonAncestor: function() {\r\n    var ancestorsLists = $A(arguments).map(function(el) {\r\n      el = $(el);\r\n      var ancestors = el.ancestors().reverse()\r\n      ancestors.push(el);\r\n      return ancestors;\r\n    });\r\n    var common = document;\r\n    Enumerable.zip.apply(ancestorsLists.shift(), ancestorsLists).each(\r\n      function(ancestorTuple) {\r\n        var cand = ancestorTuple.shift();\r\n        if (!ancestorTuple.all(function(el) { return el == cand; })) {\r\n          throw $break;\r\n        }\r\n        common = cand;\r\n      });\r\n    return common;\r\n  }\r\n});\r\n<\/pre>\n<p><\/code><\/p>\n<p>[See <a href=\"http:\/\/prototypejs.org\/api\/element\/methods\/ancestors\"><tt>Element#ancestors<\/tt><\/a>, <a href=\"http:\/\/prototypejs.org\/api\/enumerable\/zip\"><tt>Enumerable#zip<\/tt><\/a>, <a href=\"http:\/\/prototypejs.org\/api\/enumerable\/each\"><tt>$break<\/tt><\/a>.]<\/p>\n<p>Finally, here&#8217;s an example that puts the existing pieces together.<\/p>\n<p><code><\/p>\n<pre>\r\ndocument.observe('dom:loaded', function() {\r\n  document.body.collectEvents('dom:changed', function(events) {\r\n    var affectedElements = events.invoke('element');\r\n    var ancestor = Element.commonAncestor.apply(null, affectedElements);\r\n    console.log('dom:changed below: ', ancestor);\r\n  }, 10);\r\n});\r\n<\/pre>\n<p><\/code><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The DOM change notification I presented yesterday is one piece in the puzzle to rejuvenate my languishing JavaScript form validator. I&#8217;ve not yet made up my mind on a lot of issues, but there&#8217;s one thing that&#8217;s certain: the next &hellip; <a href=\"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/2008\/01\/08\/event-collector\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[8,15],"tags":[],"class_list":["post-37","post","type-post","status-publish","format-standard","hentry","category-javascript","category-prototypejs"],"_links":{"self":[{"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/posts\/37","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=37"}],"version-history":[{"count":1,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/posts\/37\/revisions"}],"predecessor-version":[{"id":120,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/posts\/37\/revisions\/120"}],"wp:attachment":[{"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=37"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=37"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.schuerig.de\/michael\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=37"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}