Facebook-style Search with Knockoutjs and JQuery

I recently stumbled across Knockoutjs, a MVVM framework for Javascript that allows us to declaratively add two-way data binding to html elements with minimal markup and no tedious event registration. Now there are plenty of examples of using Knockoutjs on the project website, but what I want to see is if I could create a Facebook style search box and results using some nifty and clean Knockoutjs code.

Now, Knockoutjs isn’t really concerned with the DOM manipulation and event registration of the current generation of javascript frameworks, but that is not to say that it doesn’t play nice. In fact, using Knockoutjs and jQuery together is just a dream.

View Models and Bindings

Our first port of call is to look at how we plan our view model. I can initially see we’re going to need something like:

var viewModel = {
  query: ko.observable(),
  results: ko.observableArray()
};

This allows us to data bind to the query property and automatically update that as our search box is changed. But the problem we currently have, is when we want to start fetching data from our server, we would have to manually update this observable with our results. To get round this, we can use a nifty extension called ko.mapping. The mapping extension allows us to define a base model and generate a view model from that, e.g.:

var baseModel = {
  query: "",
  results: []
}

var viewModel = ko.mapping.fromJS(baseModel);

The mapping extension will take our base model and generate a view model with observable members within it, essentially our mapped view model works like our original view model. What we can do now though, is when we grab results from our server, we can simple use the mapping extension to update the view model, which in turn updates our UI.

var resultModel = // get from server?
ko.mapping.updateFromJS(viewModel, resultModel);

So how does this fit in with our UI?

A simple html markup

Our markup is pretty simple, we have a textbox, a button and a list. Chuck in a few more elements and some styling, we get a nice Facebook-style search box:

<div class="searchBox">
  <span class="searchContainer"><input /><button></button></span>
  <ul class="results"></ul>
</div>

Data-binding the view model

What we need to do first, is add a two-way data-binding to our input textbox, but the default binding updates our value when the change event is fired. To allow a search-as-you-type experience, we need to change this to a keyup event. We can do that with the following binding:

<input data-bind="value: query, valueUpdate: 'afterkeyup'" />

Now, let’s jump ahead a bit, and start looking at how our UI will get updated by our view model:

(function($)
{      
  var baseModel = 
  {
    query: "",
    results: []
  };
  
  var viewModel = ko.mapping.fromJS(baseModel);
  viewModel.doSearch = function()
  {
    var $this = this;
    setTimeout(function()
    {
      var resultModel = null;
      var q = $this.query();
      if (q == "") 
      {
        resultModel = { results: [] };
        ko.mapping.updateFromJS(viewModel, resultModel);
      } 
      else
      {
        $.ajax({
          url: "json.asp",
          data: { "query": q },
          type: "GET",
          dataType: "json",
          success: function(r)
          {
            resultModel = r;
            ko.mapping.updateFromJS(viewModel, resultModel);
          }
        });
      }
    }, 1);
    
    return true;
  };

  ko.applyBindings(viewModel, $("#search").get(0));
})(jQuery);

I’ve added a function to the view model, doSearch and that is responsible for updating our results. We use jQuery’s ajax function to grab our server data (in JSON format), and then map that result straight into our view model. You’ll notice the setTimeout call, unfortunately the keypress event the doSearch function will be bound to fires before the value is updated, so we need to delay it and allow our value to update before the we try and do our search.

We’ve also only applied this view model in the scope of the search element, this allows us to use multiple view models in a single page, targeting different widgets.

Binding events and templating the results

To get it all working, we need to do a couple of things. Firstly, let’s bind our events, we do this in two places, our textbox and our button. The button will respond to the default click event, but the textbox needs to respond to keyup, so…

<input data-bind="value: query, valueUpdate: 'afterkeyup', event: { keyup: doSearch }" />
<button data-bind="click: doSearch"></button>

Using jQuery’s tmpl plugin, we can easily create client side templates, which we need for our result objects. Now, our results may look like this:

results: [
  { type: "header", text: "People" },
  { type: "person", name: "Matthew Abbott", imageUrl: "..." }
]

So, in our template, we need to handle both header elements, and people elements. Let’s have a look:

<script type="text/html" id="resultItem">
  <li class="${ type() }">
    {{if type() == "header"}}
      <span data-bind="text: text"></span>
    {{else}}
      <a href="#">
        <img src="${ imageUrl() }" />
        <span class="text" data-bind="text: name"></span>
      </a>
    {{/if}}
  </li>
</script>

With that template, we add a specific class for styling purposes, based on the result type, and then fill the content using data-bound html elements. Once again, we leave the majority of our data-binding to Knockoutjs, but we let jQuery’s tmpl plugin handle our conditionals.

And this is how we wire it up:

<ul class="results" data-bind='template: { name: "resultItem", foreach: results }, visible: results().length > 0'></ul>

We add our data binding to the list element, specify our template name, and then we toggle the visible binding based on the number of results. Without any results present, the list will automatically be hidden, when we have results, it shows. Here is an example:

Let me know what you think, you can have a look at the demo here: http://fidelitydesign.net/pub/index.html

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)