‹prev |
My Chain |
next›
To date, I have enjoyed decent success replacing the persistence layer in my
Backbone.js calendar application. With only a few hiccups, I have replaced the normal REST-based persistence layer with
faye pub-sub channels. The hope is that this will make it easier to respond to asynchronous change from other sources.
So far, I am able to fetch and populate a calendar appointment collection. I can also create new appointments. Now, I really need to get deleting appointments working:
Thanks to replacing
Backbone.sync(), in my Backbone application, I already have delete requests being sent to the
/caledars/delete faye channel:
var faye = new Faye.Client('/faye');
Backbone.sync = function(method, model, options) {
faye.publish("/calendars/" + method, model);
}
Thanks to simple client logging:
_(['create', 'update', 'delete', 'read', 'changes']).each(function(method) {
faye.subscribe('/calendars/' + method, function(message) {
console.log('[/calendars/' + method + ']');
console.log(message);
});
});
...I can already see that, indeed, deletes are being published as expected:
To actually delete things from my
CouchDB backend, I have to subscribe to the
/calendars/delete faye channel in my
express.js server. I already have this working for adding appointments, so I can adapt the overall structure of the
/calendars/add listener for
/calendars/delete:
client.subscribe('/calendars/delete', function(message) {
// HTTP request options
var options = {...};
// The request object
var req = http.request(options, function(response) {...});
// Rudimentary connection error handling
req.on('error', function(e) {...});
// Send the request
req.end();
});The HTTP options for the DELETE request are standard
node.js HTTP request parameters:
// HTTP request options
var options = {
method: 'DELETE',
host: 'localhost',
port: 5984,
path: '/calendar/' + message._id,
headers: {
'content-type': 'application/json',
'if-match': message._rev
}
};
Experience has taught me that I need to send the CouchDB revisions along with operations on existing records. The
if-match HTTP header ought to work. The
_rev attribute on the record/message sent to
/calendars/delete holds the latest revision that the client holds. Similarly, I can delete the correct object from CouchDB by specifying the object ID in the
path attribute—the ID coming from the
_id attribute of the record/message.
That should be sufficient to delete the record from CouchDB. To tell my Backbone app to remove the deleted element from the UI, I need to send a message back on a separate faye channel. The convention that I have been following is to send requests to the server on a channel named after a CRUD operation and to send responses back on a channel named after the Backbone collection method to be used. In this case, I want to send back the deleted record on the
/calendars/remove channel.
To achieve this, I do the normal node.js thing of passing an accumulator callback to
http.request(). This accumulator callback accumulates chunks of the reply into a local
data variable. When all of the data has been received, the response is parsed as JSON (of course it's JSON—this is a CouchDB data store), and the JSON object is sent back to the client:
var req = http.request(options, function(response) {
console.log("Got response: %s %s:%d%s", response.statusCode, options.host, options.port, options.path);
// Accumulate the response and publish when done
var data = '';
response.on('data', function(chunk) { data += chunk; });
response.on('end', function() {
var couch_response = JSON.parse(data);
console.log(couch_response)
client.publish('/calendars/remove', couch_response);
});
});
Before hooking a Backbone subscription to this
/calendars/remove channel, I supply a simple logging tracer bullet:
faye.subscribe('/calendars/remove', function(message) {
console.log('[/calendars/remove]');
console.log(message);
});
So, if I have done this correctly, clicking the delete icon in the calendar UI should send a message on the
/calendars/delete channel (which we have already seen working above). The faye subscription on the server should be able to use this to remove the object from the CouchDB database. Finally, this should result in another message being broadcast on the
/calendars/remove channel. This
/calendars/remove message should be logged in the browser. So let's see what actually happens...
Holy cow! That actually worked.
If I reload the web page, I see one less fake appointment on my calendar. Of course, I would like to have the appointment removed immediately. Could it be as simple as sending that JSON message as an argument to the
remove() method of the collection?
faye.subscribe('/calendars/remove', function(message) {
console.log('[/calendars/remove]');
console.log(message);
calendar.appointments.remove(message);
});
Well, no. It is not that simple. That has no effect on the page. But wait...
After digging through the Backbone code a bit, it ought to work. The
remove() method looks up models to be deleted via
get():
_remove : function(model, options) {
options || (options = {});
model = this.getByCid(model) || this.get(model);
// ...
}
The
get() method looks up models by the
id attribute:
// Get a model from the set by id.
get : function(id) {
if (id == null) return null;
return this._byId[id.id != null ? id.id : id];
},
The
id attribute was set on the message/record received on the
/calendars/remove channel:
So why is the UI not being updated by removing the appointment from the calendar?
After a little more digging, I realize that it is being removed from the collection, but the necessary events are not being generated to trigger the Backbone view to remove itself. The events that the view currently subscribes to are:
var Appointment = Backbone.View.extend({
initialize: function(options) {
// ....
options.model.bind('destroy', this.remove, this);
options.model.bind('error', this.deleteError, this);
options.model.bind('change', this.render, this);
},
// ...
});
It turns out that the
destroy event is only emitted if the default
Backbone.sync is in place. If the XHR DELETE is successful, a
success callback is fired that emits
destroy. Since I have replaced
Backbone.sync, that event never fires.
What
does fire is the
remove event. So all I need to change in order to make this work is to replace
destroy with
remove:
var Appointment = Backbone.View.extend({
initialize: function(options) {
// ....
options.model.bind('remove', this.remove, this);
options.model.bind('error', this.deleteError, this);
options.model.bind('change', this.render, this);
},
// ...
});
And it works! I can now remove all of those fake appointments:
Ah. Much better.
It took a bit of detective work, but, in the end, not much really needed to change.
I really need to cut back on my chain posts so that I can focus on writing
Recipes with Backbone. So I sincerely hope that lessons learned tonight will make updates easier tomorrow. Fingers crossed.
Day #147