Searcher – Backbone application demonstration


In this article we will build Backbone.js application along with jQuery, underscore.js and require.js. The aim of this article is to demonstrate the use of Backbone components. As we all probably know, there are more then one way to build Backbone applications so feel comfortable to adopt what you like.
At the end of this article we will have Backbone searcher application which will know to make searches using different search providers. You can see our final application in action here and can download the code here.

Application Loading Flow

index.html

Let’s begin with our application loading flow. After typing the url, the browser starts loading the index.html file:

index.html
<!doctype html>
<html>
    <head>
        <title></title>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <link rel="stylesheet" href="css/style.css" type="text/css"/>       
        <script data-main="js/main" src="assets/js/require.js"></script>
    </head>
    <body>
        <header>
            <nav class="search"></nav>
        </header>
        <div class="container">
            <aside class="side-bar">
                <div id="term-history"></div>
            </aside>
            <section class="content">
            </section>
        </div>
    </body>
</html>

The index.html file contains the layout of our application which include placeholders for the search section, the history section and the search results area. It also includes reference to css file and reference to the require.js script.
When the browser loads this html file, right after loading style.css, the browser loads the require.js script. Look closely and you’ll notice that require.js script tag has additional attribute called “data-main”. This attribute tells require.js to load js/main.js after require.js loads.

js/main.js

This file contains two sections:

  • Configuration section that configure the require.js paths and modules.
  • Initialization section that initialize the application.
js/main.js
require.config({
    paths: {
        jQuery: '../assets/js/jquery-1.9.0.min',
        Underscore: '../assets/js/underscore-min',
        Backbone: '../assets/js/backbone-min',
        tooltipster: '../assets/js/jquery.tooltipster',
        text: '../assets/js/text'
    },
    shim: {
        'jQuery': {
            exports: '$'
        },
        'Underscore': {
            exports: '_'
        },
        'Backbone': {
            deps: [
                'Underscore',
                'jQuery'
            ],
            exports: 'Backbone'
        },
        'tooltipster': {
            deps: [
                'jQuery'
            ]
        }
    }
});

require([
    'Backbone',
    'router',
    'app'
], function(Backbone, Router, app) {

    var router = new Router();
    app.initialize(router);

    Backbone.history.start();
});

require.js configuration allows us to map modules paths to names. For example, jQuery.js file is located in “libs/jquery-1.8.2.min”. Whenever we wish mark jQuery as a dependency, we will have to write this long path. Since jQuery is basic module and we probably use it a lot, it is better to map its path.
require.js works with AMD modules. The AMD structure tells require.js what are the dependencies and which object to return. The purpose of the shim configuration is to tell require.js for each un-AMD module what is its dependencies and which object to return.
After the configurations done, we ask require.js to load Backbone, router.js and app.js, and after that execute the initialization function. This function gets as parameters the AMD modules that require.js required to resolve and initializes the router, initializes the app and starts Backbone.history.

app.js

js/app.js
define([
    'Underscore',
    'Backbone',
    'models/query',
    'views/search',
    'views/history',
    'sources/sources-manager',
    'models/source',
    'sources/library-of-congress/views/grid',
    'sources/google-search-api-for-shopping/views/list'
], function(_, Backbone,
        QueryModel, SearchView, HistoryView,
        SourcesManager, SourceModel,
        LocGridView, GoogleListView) {

    var Application = function() { };

    _.extend(Application.prototype, {
        initialize: function (router) {
            this.router = router;
            this.appQuery = new QueryModel();

            this.appQuery.on('change', function(model, changes) {
                this.router.navigate(
                    '/search/' + model.get('sourceId') + '/' + model.get('term'),
                    {trigger: false } );
            }, this);

            this.searchView = new SearchView({
                model: this.appQuery
            });
            this.historyView = new HistoryView({
                model: this.appQuery
            });

            this.sourcesManager = new SourcesManager( {
                el: '.content',
                model: this.appQuery,
                sources: [
                    new SourceModel({
                        id: 'library-of-congress',
                        name: 'Library Of Congress',
                        view: LocGridView
                    }),
                    new SourceModel({
                        id: 'google-shopping',
                        name: 'Google Shopping',
                        view: GoogleListView
                    })
                ]
            });

            this.searchView.addSources(this.sourcesManager.sourcesPool);
        }
    });

    return new Application();
});

Lets see what app.js initialization function does:

  • Keeps reference of the router and initializes instance of QueryModel. This appQuery instance acts as a singleton and every time it changes, the router changes the url to “search/<sourceId>/<term>” (without trigger a route event).
  • Initializes the main views of the application – SearchView and HistoryView.
  • Creates two new search sources. For each source it is necessary to know it’s name, id (for internal purposes) and it’s main view. Later we will discuss on the sources feature.
  • Creates sourceManager that knows manage search sources (we will discuss on it later also) and adds to it the two search sources.
  • Adds the two search sources to the search view.

At this point, the application loading flow is over and now the application waits for user interaction.
In order to understand completely how everything bonds together and works, we must understand the application features and components.

router.js

js/router.js
define([
    'Backbone',
    'app'
], function(Backbone, app) {
    var Router = Backbone.Router.extend({
        routes: {
            'search/:sourceId/:term': 'searchImages'
        },
        searchImages: function(sourceId, term) {
            app.appQuery.set( { sourceId: sourceId, term: term } );
        }
    });
    return Router;
});

The router depends on app.js. Whenever a route in form “search/<sourceId>/<term>” is entered to the url, the router trigger the searchImages() method which changes the appQuery singleton.

Application Features and Components

Now it is time to review the searching, sources and the history features.

Searching

The main purpose of the application is to allow searching. The application makes searches among different search providers, therefore the input it gets from the user contains a search term and a search provider. So, we need a model to store this information. Actually, a single instance of this model will serve us during the entire use of the application. Each time the user makes a different search (change the search term or provider), the model instance changes. Later, those model changes will trigger the search.

js/models/query.js
define([
    'Underscore',
    'Backbone'
], function(_, Backbone) {
    var QueryModel = Backbone.Model.extend({
        defaults: {
            term: '',
            sourceId: ''
        }
    });
    return QueryModel;
});

QueryModel has two attributes. “term” for holding the search term and “sourceId” for holding the search provider. The default value for both attributes are the empty string.

SearchView view creates the inputs and adds the behavior of the searching process.

{% include_code lang:javascript js/views/search.js searcher/js/views/search.js %}

SearchView renders itself on initialization, and every time appQuery changes it updates the input values. On render, the view draws itself using underscore templates and initializes the inputs according to the appQuery. Whenever the user clicks on the search button, the view set appQuery with the new values which causing the url to change (as we saw in app.js). Notice that SearchView uses the text plugin of require.js in order to load templates/search.html. In addition, the compiled version of templates are stored in searchTemplate and in optionTemplate in order to save compilations. SearchView contains the addSource() method which gets sourceModel instance as parameter (we will see it later) and adds the new source to the sources select list.

Sources

As I mentioned before, the application makes searches among different search providers. The sources mechanism is responsible for defining search providers, their models and their views. This feature includes the SourcesManager view which acts as a bridge and responsible for rendering the relevant search results according to appQuery.

js/sources/sources-manager.js
define([
    'jQuery',
    'Underscore',
    'Backbone',
    'text!templates/loading.html'
], function($, _, Backbone, loadingTemplate) {

    var SourcesManager = Backbone.View.extend({
        loadingTemplate: _.template(loadingTemplate),
        initialize: function(options) {
            this.model.on('change', this.render, this);
            this.sourcesPool = { };
            if (options.sources) {
                for ( var i = 0; i < options.sources.length; i++ ) {
                    this.addSource(options.sources[i]);
                }
            }
        },
        render: function(model, changes) {
            var sourceId = this.model.get('sourceId');
            var sourceModel = this.sourcesPool[sourceId];
            if (!sourceModel) {
                console.log('Source ' + sourceId + ' not found!');
                return;
            }

            var term = this.model.get('term');
            var viewType = sourceModel.get('view');
            var view = new viewType({
                el: this.el
            });

            console.log('Rendering ' + sourceId + ' with term "'+ term + '"');

            this.$el.empty().append(this.loadingTemplate());
            view.render({ term: term });
        },
        addSource: function(sourceModel) {
            var sourceId = sourceModel.get('id');
            this.sourcesPool[sourceId] = sourceModel;
            console.log('Adding source ' + sourceId + ' to the sources pool');
        }
    });

    return SourcesManager;
});

When initialized with appQuery as model, SourcesManager renders itself on appQuery change. SourceManager has the ability to add sources using the addSource() function or using the initialization “sources” option. When it renders, it resolves the search provider’s view according to appQuery and renders it.
SourceManager initialization occurs inside the application initialization:

SourcesManager initialization
this.sourcesManager = new SourcesManager( {
    el: '.content',
    model: this.appQuery,
    sources: [
        new SourceModel({
            id: 'library-of-congress',
            name: 'Library Of Congress',
            view: LocGridView
        }),
        new SourceModel({
            id: 'google-shopping',
            name: 'Google Shopping',
            view: GoogleListView
        })
    ]
});

appQuery is the SourcesManager model and the search results are rendered inside “.content” element. The search providers are also defined here using the SourceModel. Each search provider should have id, name and main view which will be displayed when selected.

js/models/source.js
define([
    'Backbone'
], function(Backbone) {
    var SourceModel = Backbone.Model.extend({
        defaults: {
            id: '',
            name: '',
            view: null
        }
    });
    return SourceModel;
});

Google Shopping search provider

Let’s explore the Google Shopping search provider. It’s files located under js/sources/google-search-api-for-shopping and it consist of ProductModel, ProductsCollection, products template and it’s main view called ListView.

js/sources/google-search-api-for-shopping/models/product.js
define([ 'Backbone' ], function(Backbone) {
    var ProductModel = Backbone.Model.extend({
        defaults: {
            title: '',
            description: '',
            link: '',
            thumbnail: ''
        },
        parse: function(item) {
            var attrs = { };
            if (item && item.product) {
                var product = item.product;
                attrs.title = product.title || '';
                attrs.description = product.description || '';
                attrs.link = product.link || '';

                if (product.images && product.images.length > 0 &&
                    product.images[0].status == 'available' &&
                    product.images[0].thumbnails && product.images[0].thumbnails.length > 0) {
                    attrs.thumbnail = product.images[0].thumbnails[0].link;
                }
            }
            return attrs;
        }
    });
    return ProductModel;
});

Each product contains title, description, link for the product and a small thumbnail. The parse method is used by Backbone in order to parse the response of single product, when fetching the data from Google.

js/sources/google-search-api-for-shopping/collections/products.js
define([
    'Backbone',
    'sources/google-search-api-for-shopping/models/product'
], function(Backbone, ProductModel) {
    var ProductsCollection = Backbone.Collection.extend({
        model: ProductModel,
        url: 'https://www.googleapis.com/shopping/search/v1/public/products',
        parse: function(response) {
            return response.items;
        }
    });
    return ProductsCollection;
});

The products collection consist of ProductModel models and the url attribute is used by Backbone fetch method.

js/sources/google-search-api-for-shopping/templates/products.html
<table class="list-table">
    <thead>
        <tr>
            <th>Image</th>
            <th>Description</th>
        </tr>
    </thead>
    <tbody>
    <% _.each(products, function(product) { %>
        <tr>
            <td><img alt="" src="<%= product.thumbnail %>" class="img-polaroid"/></td>
            <td>
                <h4><a target="_blank" href="<%= product.link %>"><%= product.title %></a></h4>
                <p><%= product.description %></p>
            </td>
        </tr>
    <% }); %>
    </tbody>
</table>

The search results structure defined inside the products.html as a table that contains all the products. For each product, a new product row with thumbnail, title and description is created.
Search providers can contain many views. When defining the search provider in SourcesManager, we must tell which view is the main view to display. ListView is the main view of the Google Shopping search provider.

js/sources/google-search-api-for-shopping/views/list.js
define([
    'jQuery',
    'Underscore',
    'Backbone',
    'sources/google-search-api-for-shopping/collections/products',
    'sources/google-search-api-for-shopping/models/product',
    'text!sources/google-search-api-for-shopping/templates/products.html'
], function($, _, Backbone, ProductsCollection, ProductModel, productsTemplate) {
    var ListView = Backbone.View.extend({
        template: _.template(productsTemplate),
        initialize: function() {
            this.products = new ProductsCollection();
        },
        render: function(options) {
            this.products.fetch({
                data:{
                    key: 'AIzaSyDEMpzAwWS40E6TBjIA_XH76QfO0YSsvDc',
                    country: 'US',
                    fields: 'items(product(title,description,link,images(status,thumbnails(link))))',
                    q: options.term,
                    alt: 'json',
                    thumbnails: '128:128'
                },
                success: _.bind(function(collection, response) {
                    this.$el.empty();
                    if (this.products.size() > 0) {
                        this.$el.append(this.template({products: this.products.toJSON()}));
                    } else {
                        this.$el.text('No result found!');
                    }
                }, this),
                error: _.bind(function(collection, xhr, options) {
                    this.$el.empty().text('Error get result!!');
                }, this)
            });
        }
    });
    return ListView;
});

ListView initializes ProductsCollection and on render fetch it and append the results to el. In case of an error or empty results, a relevant text message appears. Behind the scenes, the fetch method uses the jQuery.ajax function and the data option is passed to it. The data option contains needed properties for the Google Shopping api. Keep in mind that in your application you will need to use yours Google api key.
Now, whenever the user chooses Google Shopping as a search provider, SourcesManager initializes ListView which fetches the results and display them inside “.content” element.

History

Another feature of this application is history. The application stores queries history and enables us to make searches from history. In order to store the queries history we need a collection of QueryModel:

js/collections/queries.js
define([
    'Underscore',
    'Backbone',
    'models/query'
], function(_, Backbone, QueryModel) {
    var QueriesCollection = Backbone.Collection.extend({
        model: QueryModel
    });
    return QueriesCollection;
});

Now, in order to display the history and make each history entry clickable, There is the HistoryView:

js/views/history.js
define([
    'jQuery',
    'Underscore',
    'Backbone',
    'app',
    'collections/queries',
    'text!templates/queries-list.html'
], function($, _, Backbone, app, QueriesCollection, queriesListTemplate) {
    var HistoryView = Backbone.View.extend({
        el: '#term-history',
        events: {
            'click a': 'setModel'
        },
        initialize: function() {
            this.queriesCollection = new QueriesCollection();
            this.model.on('change', this.addQuery, this);
        },
        addQuery: function(model) {
            this.queriesCollection.push(this.model.clone());
            this.render();
        },
        setModel: function(e) {
            var term = $(e.currentTarget).data('term');
            var sourceId = $(e.currentTarget).data('source');
            this.model.set( { term: term, sourceId: sourceId } );
        },
        template: _.template(queriesListTemplate),
        render: function() {
            this.$el.html(this.template({'queries': this.queriesCollection.toJSON()}));
        }
    });
    return HistoryView;
});

HistoryView gets appQuery as a model, and on initialization it creates QueriesCollection instance. Whenever the appQuery changes, the view adds it to queries collection and renders itself. Render takes the queries collection and generates the markup from the queriesListTemplate dependency. Whenever the user click on history entry, the setModel() function triggered and set appQuery with the history values. As a result of appQuery change, SourcesManager’s render occurred.