Thursday, January 19, 2012

Delete (XHR and DOM) in Dart

‹prev | My Chain | next›

As of last night, I have my little comic book Dart application querying the data store over REST and populating the web page thusly:


Tonight I would like to attach handlers to those delete links. If possible, I would like the handlers to DELETE the record from the backend and, on successful delete, I would like to remove the item from the page.

First up I attach the delete handlers after the comic books have been loaded and populated on the page:
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();
}
Having done quite a bit of Backbone.js coding recently, I am already fighting an intense urge to convert this into a collection fronted by a collection view / item view. I will experiment with that later—for now, I will just get this working.

As for actually adding the delete event handler, I query the list element for all delete items. For each of them, I add an on-click handler:
attach_delete_handlers(parent) {
  parent.queryAll('.delete').forEach((el) {
    el.on.click.add((event) {
      print("id: " + event.target.parent.id);
      event.preventDefault();
    });
  });
}
For now, I just print out the ID of the containing LI tag, in which, as can be seen in the screenshot above, I am storing the ID of the element in the data store. Then I prevent the default event bubbling from occurring so that no navigation takes place.

And that works. Clicking the delete links prints out the IDs of the comic books in question:


In my express.js + node-dirty backend, I have a simple delete route:
app.delete('/comics/:id', function(req, res) {
  db.rm(req.params.id);
  res.send('{}');
});
With that, I update the delete handlers to call a new delete() function:
attach_delete_handlers(parent) {
  parent.queryAll('.delete').forEach((el) {
    el.on.click.add((event) {
      delete(event.target.parent.id);
      event.preventDefault();
    });
  });
}
In the delete() function, I open an Ajax request with the delete HTTP verb and the proper ID:
delete(id, [callback]) {
  var req = new XMLHttpRequest()
    , default_callback = () { print("[delete] $id"); };

  req.on.load.add((res) {
    (callback || default_callback)();
  });

  req.open('delete', '/comics/$id', true);
  req.send();
}
In there, I have added an on-load handler that will invoke an optional callback. If the callback is not supplied (I have not supplied it yet), then the default_callback() is invoked.

Except it isn't. When I try this out, I am greeted by:
Exception: Object is not closure
Stack Trace:  0. Function: '::function' url: 'http://localhost:3000/scripts/comics.dart' line:37 col:35
 1. Function: 'EventListenerListImplementation.function' url: 'dart:htmlimpl' line:23183 col:35
The "Object is not a closure" error message in Dart is quickly becoming my least favorite. It usually takes me a while to figure out what it is trying to tell me.

What it is telling this time, it turns out, is that the result of (callback || default_callback) is a boolean, not one or the other functions. Dang. Looks like short-circuit evaluations don't behave as I hoped in Dart. Thus I am forced to use a slightly more verbose ternary:
delete(id, [callback]) {
  var req = new XMLHttpRequest()
    , default_callback = () { print("[delete] $id"); };

  req.on.load.add((res) {
    (callback != null ? callback : default_callback)();
  });

  req.open('delete', '/comics/$id', true);
  req.send();
}
That does work as I hope as I see my default [delete] message logged to the console:


The last thing that I would like to get done tonight is removing the list item from the DOM once it has been successfully removed from the backend store. This is where that optional callback parameter of delete() comes in handy.

I specify the optional callback argument to delete when I attach the delete handler. In addition to printing the ID of the item being deleted, I also remove the element from the DOM:
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();
    });
  });
}
I am just guessing on the remove() method for the element. But, given what I know of Dart, I am reasonably sure that will work because, as a long-time Javascript developer, that is what I would use.

And it works! Not only is the item removed from the backend, but it is also removed from the UI:


With delete from XHR and delete from the DOM both working, I call it a night. Up tomorrow, I may try to refactor this a bit. Or I might play with animations.


Day #270

No comments:

Post a Comment