TIL: Knockout View Models with jQuery Deferreds
I was working on a project that relies heavily on knockout.js for data-binding and its MVVM awesomeness. Inside my view model, I had a method that made a REST API call and I wanted to do something unrelated to the view model when that call finished. Simple right? Just use a callback! That’s what I thought.
What is a jQuery Deferred?You can read more about jQuery deferreds on their site and more about promises at CommonJS Promises/A design. It boils down to receiving notification when a synchronous or asynchronous function has completed (either successfully or not) and executing arbitrary code in turn.
Why Deferreds?Why not just use a callback parameter? Sure, that would work. In fact, my first instinct was to pass in a callback function as a parameter to the method. I do this in C#, so I wrote the code and it ran as expected. The main issue is that it’s messy and had nothing to do with the view model. It’s only there for program flow. My view model felt dirty. Someone pass the granola.
Using deferreds allows me to run code after the asynchronous call completes.Sure, jQuery has callbacks all over the place and all those callbacks are parameters to some function somewhere in the framework. If it’s good enough for them, who am I to question? True, but deferreds were introduced in jQuery 1.5. They made it better. We can use the callback method, but deferreds give us a better way (depending on the circumstances).
Any other options?There were two other options I tried: parent references and events. For the parent reference, I had passed the view model a reference to the parent that created it. This allowed me to call methods on the parent from within the child. It’s kind of like watching drunk ugly people make out at a wedding reception. Nasty. That’s why I went looking for something else.
I also tried subscribing to and firing custom events. This is common in .NET so I’m used to programming this way. I used the jQuery methods .bind() and .triggerHandler() to accomplish this. It’s not bad. But it seemed like I was trying too hard. There was just a lot of housekeeping that I didn’t want to do.
This is an example of what I started with: You can see this in action over on JSFiddle
See that I’m directly calling the view model’s method and inside that AJAX call, I do work inside the success callback. In this trivial case, it’s okay, but what I was trying to do required the caller to do work after the call returned that had nothing to do with the view model. Unfortunately, because the AJAX call is asynchronous, the method call was returning right away even though the AJAX call hadn’t finished yet. I had a race condition that wouldn’t necessarily show up during debugging.
Using deferreds allows me to run code after the asynchronous call completes. In the code above, note that this method calls the function $.ajax() which is the jQuery way of doing an AJAX op. This is lucky for me because the $.ajax() already implements promises so there’s only a single change I have to make:
I need to change the method to return the AJAX call. By doing this, we implicitly return a promise to the caller. This, therefore, allows us to use the $.when() and $.then() calls.
Here’s the JSFiddle for the updated version.
That’s it! I can now chain together lots of code to run after the method actually completes without having to muck up my method with callback parameters, parent references, or event handlers.