I’ve been doing so many cool things with jQuery and Knockout lately that have my developer soul bursting with glee. One of the things that I did most recently was to implement a small search application that allows a user to execute multiple searches and display the results as a collection of results. (That’s a little confusing. What I mean is that when you do one search, you get a box with the results. If you do another search, you get another box with another set of results.)
I wanted the interface to feel really fluid, and I figured the best way to do this would be to animate the appearance of the search results. I was using Knockout to databind results to the page, but this caused a problem: the results would appear on the page as soon as they were added to the collection.
Let’s look at an example of what I’m talking about so far:
var gcounter = 0; var viewModel = function () { var self = this; self.items = ko.observableArray(); self.add = function () { self.items.push(new itemViewModel("item " + ++gcounter)); } self.remove = function (item) { self.items.remove(item); }; }; var itemViewModel = function(data) { var self = this; self.text = ko.observable(data); }; ko.applyBindings(new viewModel());
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> <div class="inline clickable" data-bind="click: add"> <span class="ui-icon ui-icon-plus inline"></span> <span>Add</span> </div> <div data-bind="foreach: items"> <div class="bigredbox clickable" data-bind="click: $parent.remove"> <span class="ui-icon ui-icon-minus inline"></span> <span data-bind="text: text"></span> </div> </div>
This example allows you to click ‘Add’ to add items, and remove items by clicking them. It’s very responsive, but it lacks a certain je ne sais quoi since items simply appear and disappear instantly. My first thought on addressing this was to make items fade into existence as they are added. We can do this in two steps. First, change the template for the data item to be hidden by default. Then, use jQuery to fade-in after the item is added to the collection. (Note that my hidden class contains just display: none and is defined in my css.)
var gcounter = 0; var viewModel = function () { var self = this; self.items = ko.observableArray(); self.add = function () { self.items.push(new itemViewModel("item " + ++gcounter)); $(".bigredbox.hidden").fadeIn(); } self.remove = function (item) { self.items.remove(item); }; }; var itemViewModel = function(data) { var self = this; self.text = ko.observable(data); }; ko.applyBindings(new viewModel());
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> <div class="inline clickable" data-bind="click: add"> <span class="ui-icon ui-icon-plus inline"></span> <span>Add</span> </div> <div data-bind="foreach: items"> <div class="bigredbox clickable hidden" data-bind="click: $parent.remove"> <span class="ui-icon ui-icon-minus inline"></span> <span data-bind="text: text"></span> </div> </div>
Ooh, that feels nice now, doesn’t it? But removing items still feels kinda lame. It would be really cool if items would fade away when they were removed! We can do that similarly to how we added the item. We need to employ a few tricks to make this happen, though. The click event’s second argument contains the control that was clicked in its target property. So, we can use event.target along with jQuery’s closest function to find the control we need to fade out. jQuery’s fadeOut method allows you to specify a callback to execute once it’s complete, so we’ll fade-out the selected item and remove it from the collection once the fade completes. That’s all we need to do, though; no HTML changes are needed.
var gcounter = 0; var viewModel = function () { var self = this; self.items = ko.observableArray(); self.add = function () { self.items.push(new itemViewModel("item " + ++gcounter)); $(".bigredbox.hidden").fadeIn(); } self.remove = function (item, event) { $(event.target).closest(".bigredbox").fadeOut(null, function() { self.items.remove(item); }); }; }; var itemViewModel = function(data) { var self = this; self.text = ko.observable(data); }; ko.applyBindings(new viewModel());
So that’s working great, but I still have a problem. When an item is removed, it fades away nicely, but then the results just flash together once the item disappears. It’d be nicer if that was also animated. jQuery’s slideUp method seems like it’d be perfect for this, so let’s just chain it together with fadeOut! Once again, there’s a little trick to this: you need to specify that the animations shouldn’t be queued. Luckily, that’s accomplished easily by specifying the correct option. Once again, this is a javascript-only change.
var gcounter = 0; var viewModel = function () { var self = this; self.items = ko.observableArray(); self.add = function () { self.items.push(new itemViewModel("item " + ++gcounter)); $(".bigredbox.hidden").fadeIn(); } self.remove = function (item, event) { $(event.target).closest(".bigredbox") .slideUp({queue: false}) .fadeOut(null, function() { self.items.remove(item); }); }; }; var itemViewModel = function(data) { var self = this; self.text = ko.observable(data); }; ko.applyBindings(new viewModel());
Wooo, doggy! Now things are feeling very fluid. When we add an item, it fades into existence. Removing an item slides up and fades out. Everything feels really smooth and slick. Be sure to check out the jsfiddle for a working example!