Wednesday, August 31, 2011

Backbone.js: Telling the View to Delete the Model, Which Tells the View to Delete Itself

‹prev | My Chain | next›

I think that I have more or less figured out how to delete things in Backbone.js. I also have a halfway decent, event driven means of removing deleted things from the UI. But, to date, I am doing all of that deleting in the Javascript console. Tonight, I would like to add a UI element to do the deleting.

Adding the UI element is easy enough. I add an "X" inside a <span> with a class of "delete" to my calendar event template:
<script type="text/template" id="calendar-event-template">
  <span class="event" title="<%= description %>">
      <%= title %>
      <span class="delete">X</span>
  </span>
</script>
On the page, the "X" displays like:
To hook that "X" up to a function call, I need to add a click event to the View. For now, I stick with tracer bullets:
window.EventView = Backbone.View.extend({
      // ...
      events: {
        'click .delete': 'test'
      },
      test: function() {console.log("delete")},
    });
So, when a click event is received in this view for an element with the delete class, the "test" function is called. Sure enough, clicking on the X inside the delete <span> logs "delete" messages to the console:
Cool beans, but that is not the final target I hope to hit. So, I replace the test function with a handler for the "click .delete" event:
window.EventView = Backbone.View.extend({
      // ...
      events: {
        'click .delete': 'deleteClick'
      },
      deleteClick: function() {
        this.model.destroy();
      },
      remove: function() {
        $(this.el).find('.event').remove();
      }
    });
It feels a little awkward having a remove() method and a deleteClick(). The former removes the UI element from the page. The latter handles clicks that should signal the model to delete itself, which will, in turn, tell the view to remove itself from the page. I will worry about the odd feeling another day. For now, I am not quite done with my delete.

I am telling a CouchDB store to delete a record. Since CouchDB uses optimistic locking, I need to supply the revision ID when deleting the record. The revision is already stored in the model, so I have been deleting like this:
e.destroy({headers: {'If-Match':e.model.get("_rev")} })
It seems really wrong to me that the View should be responsible for knowing about this. But how to get the model to do this? I could create a new destroyWithRevision method on the model, but the view would still need to know to call this instead of the conventional destroy() method.

Luckily, Backbone.js does support overriding methods and calling the superclass's original method:

    window.Event = Backbone.Model.extend({
      // ...
      destroy: function() {
        Backbone.Model.prototype.destroy.call(this, {
          headers: {'If-Match': this.get("_rev")}
        });
      }
    });
That is slick. I call the destroy function that resides on the Backbone.Model prototype. Since I am invoking that function directly, I need to supply an object instance so that the method has a this (or self if you're a Rubyist) to which it can refer. That is the first argument to a Javascript call method. Then I can supply the arguments that inform CouchDB of the revision being deleted.

Slick indeed. Now, when the view tells the model to delete itself, it can call the very conventional destroy() method—completely unaware of this complexity. As an added bonus, I am not losing any of the benefits of optimistic locking—if the loaded model was superseded before the user clicked the "X", the delete would fail. And yes, when I click the little "X", the calendar event goes away from the UI:

video

That is a good stopping point for tonight. Up tomorrow, I think that my partner in crime on the Recipes with Backbone book has given me some food for thought on how to improve my view.


Day #130

No comments:

Post a Comment