Sunday, January 22, 2012

Delegated Events in Dart

‹prev | My Chain | next›

I do not believe that Dart has an equivalent to jQuery's delegated events, but tonight I take a look. As the jQuery documentation states, delegated events are nice for two reasons: specifying handlers for elements not yet added to the page and significantly cutting down on the total number of handlers in some circumstances. It was while working with Backbone.js that I especially came to know and love delegated events, hence the desire for them in Dart.

In my comic book application, I am inserting a list of comic books into a static page via XHR:


After list is inserted, I attach delete handlers to each of those Delete links:
load_comics() {
  var list_el = document.query('#comics-list')
    , req = new XMLHttpRequest();

  req.open('get', '/comics', true);

  req.on.load.add((res) {
    var list = JSON.parse(req.responseText);
    list_el.innerHTML = graphic_novels_template(list);
    attach_delete_handlers(list_el);
  });

  req.send();
}
What I would like to do is move that attach_delete_handers() function before the XHR request:
load_comics() {
  var list_el = document.query('#comics-list')
    , req = new XMLHttpRequest();

  attach_delete_handlers(list_el);

  req.open('get', '/comics', true);

  req.on.load.add((res) {
    var list = JSON.parse(req.responseText);
    list_el.innerHTML = graphic_novels_template(list);
  });

  req.send();
}
If the event handlers was in delegate mode, then this would still work. Unfortunately it does not. Clicking any of the links incurs the default behavior of the <a> tags—in this case requesting the current resource with a hash tag:


The reason that I do not believe that Dart supports this is the manner in which event handlers are added. All elements have an on getter, which returns an ElementEvents. Since on does not support any arguments, there is no mechanism to record a selector that might put the listener into delegate mode. The ElementEvents object exposes a series of EventListenerList getters (e.g. click, mouseOver, etc) to which event listeners can be added. I see no room for delegation anywhere.

This can be seen in my attach_delete_handlers() function, which iterates through each existing Delete link of the supplied UL list, attaching he same event listener to each:
attach_delete_handlers(parent) {
  parent.queryAll('.delete').forEach((el) {
    el.on.click.add((event) {
      delete(event.target.parent.id, callback:() {
        print("[delete] ${event.target.parent.id}");
        event.target.parent.remove();
      });
      event.preventDefault();
    });
  });
}
If I call attach_delete_handlers before the XHR request, then the queryAll('.delete') call will return an empty list and thus no handlers are attached.

Since that won't work (unless I am overlooking something), let's try adding a handler to the UL parent element. That handler should look through all of the .delete links and handle the event if the event target is on of those:
attach_delete_handlers(parent) {
  parent.on.click.add((event) {
    if (parent.queryAll('.delete').indexOf(event.target) == -1) return;

    print(event.target.parent.id);
    event.preventDefault();
  });
}
Sadly, when I click the delete link, I get the following:
Exception: Not impl yet. todo(jacobr)
Stack Trace:  0. Function: 'FrozenElementList.indexOf' url: 'dart:htmlimpl' line:22497 col:5
 1. Function: '::function' url: 'http://localhost:3000/scripts/comics.dart' line:59 col:43
 2. Function: 'EventListenerListImplementation.function' url: 'dart:htmlimpl' line:23183 col:35
Dang it. OK it is a new language. Things like that are bound to happen. Perhaps some() will work where indexOf() did not:
attach_delete_handlers(parent) {
  parent.on.click.add((event) {
    if (parent.queryAll('.delete').some((el) { el == event.target;})) return;

    print(event.target.parent.id);
    event.preventDefault();
  });
}
Now when I click a Delete link I get:
Exception: Not impl yet. todo(jacobr)
Stack Trace:  0. Function: 'FrozenElementList.some' url: 'dart:htmlimpl' line:22434 col:5
 1. Function: '::function' url: 'http://localhost:3000/scripts/comics.dart' line:59 col:40
 2. Function: 'EventListenerListImplementation.function' url: 'dart:htmlimpl' line:23183 col:35
Shakes fist at jacobr!

So indexOf does not work, nor does some. I know that forEach() works for certain, so...
attach_delete_handlers(parent) {
  parent.on.click.add((event) {
    var found = false;
    parent.queryAll('.delete').forEach((el) {
      if (el == event.target) found = true;
    });
    if (!found) return;

    print(event.target.parent.id);
    event.preventDefault();
  });
}
Not the prettiest code, but I can live with it until jacobr implements cleaner iterator methods.

With that working, I can add my XHR delete call to this psuedo-delegated event handler:
attach_delete_handlers(parent) {
  parent.on.click.add((event) {
    var found = false;
    parent.queryAll('.delete').forEach((el) {
      if (el == event.target) found = true;
    });
    if (!found) return;

    print(event.target.parent.id);

    delete(event.target.parent.id, callback:() {
      print("[delete] ${event.target.parent.id}");
      event.target.parent.remove();
    });

    event.preventDefault();
  });
}
And, now I can delete comics again:


That is not ideal, but at least it is possible. jQuery still has Dart beat in this respect, but I can live with this for now...

Day #273

No comments:

Post a Comment