Backbone App - End to End: Connecting to an external API

This article is the third article in the Backbone App - End to End series. In the previous article we created a basic design pattern to structure your backbone app.
In this article we shall hook our previous work with a live external API that contains movie data, and we will hook it up with our search page.
Right now you should have:

Movie Model:

MovieApp.Models.Movies = Backbone.Model.extend({

    initialize: function(options)   {},
});

Movie Collection:

var Movies = Backbone.Collection.extend({

        initialize: function(options) {}
}); 

Movie, home and search page Views:

MovieApp.Views.MovieView = Backbone.View.extend({

    initialize: function(options) {}
});
MovieApp.Views.Home = Backbone.View.extend({

    initialize: function(options) {},

    render: function()
    {
        this.$el.html("This is my home page!!");       
        return this;
    }
});
MovieApp.Views.Search = Backbone.View.extend({

    initialize: function(options) {},

    render: function()
    {
        this.$el.html("This is my search page!!");       
        return this;
    }
});

Router:

MovieApp.Router = Backbone.Router.extend({

    routes: {
        'home': 'home',
        'search': 'search',
        '*path': 'home'
    },

    home: function(){
        var view = new MovieApp.Views.Home();
        $('#main').html(view.render().el);
    },

    search: function(){
        var view = new MovieApp.Views.Search();
        $('#main').html(view.render().el);
    }
});

all glued together with a global MovieApp object:

var MovieApp = {

    Views: {},
    Models: {},
    Collections: {},
    Router: {}
}

$(document).ready(function(){
    MovieApp.Router.Instance = new MovieApp.Router();    
    Backbone.history.start();
});

Here is the current index.html file aswel:

<!doctype html>

<html lang="en">  
    <head>
        <meta charset="UTF8">
        <title>Backbonejs movie SPA</title>

        <script type="text/javascript" src="vendor/jquery/dist/jquery.js"></script>
        <script type="text/javascript" src="vendor/underscore/underscore.js"></script>
        <script type="text/javascript" src="vendor/backbone/backbone.js"></script>
        <link href='css/main.css' rel='stylesheet' type='text/css'>
    </head>
    <body>
        <div id="main-menu">
            <ul>
                <li><a href="#add">Home</a></li>
                <li><a href="#search">Search</a></li>
            </ul>
        </div>
        <div id="main"></div>
        <div id="footer"></div>

        <script type="text/javascript" src="js/App.js"></script>
        <script type="text/javascript" src="js/Models/Movie.js"></script>
        <script type="text/javascript" src="js/Collections/Movies.js"></script>
        <script type="text/javascript" src="js/Views/Movie.js"></script>
        <script type="text/javascript" src="js/Views/home.js"></script>
        <script type="text/javascript" src="js/Views/search.js"></script>
        <script type="text/javascript" src="js/Router.js"></script>
    </body>
</html>  

At this point navigation between the search page and the home page is possible. Let's hook this up with an external API, naming, the IMDB one.

The API

What's an API?

An API is basically, in simple terms, some service available through a URL (endpoint) where an app can request information from. Think of it as an URL that instead of returning a page's HTML, returns some other information.

The IMDB API

There is an official IMDB API, however, it's hopelessly undocumented and it has some other issues that i'm not going through right now. Luckily, there are quite a few unofficial ones that are better documented and slightly more straightforward to use. For the purpose of this article and for what backbone is concerned, which one we use is of no consequence, so I will just use the simplest one, the OMDB API.

For this example we will use the search endpoint, that has the following structure:

http://www.omdbapi.com/?s=<movie title>  

This request will return up to 10 results that match the movie title string. Let's set this up.

Connecting backbone with the API

Backbone can preform requests through its Models or Collections. When requesting data, the result will be parsed into said Model or Collection. In this case, since we are requesting a set of data, we are going to request the OMDB API through a collection. In the end we will have a Collection filled with 10 Movie Models.

Setting up the View

Our search page will contain only an input field, a submit button, and a container block to show the results. We are not going for pretty here.
First lets create the html. I added a view variable with the actual html, and then push it into the view's html, like so:

MovieApp.Views.Search = Backbone.View.extend({

    template: "<input type='text' placeholder='search'> \
               <button>Search movie</button> \
               <ul id='movie-list'></ul>",

    ...

    render: function()
    {
        this.$el.html(this.template);
        return this;
    }

Now lets create a function to make the API request:

getmovies: function() {

        var title = this.$el.find('input').val();
        var movies = new MovieApp.Collections.Movies({title: title});

        movies.fetch();
    }

We are getting the value from the input field with the this.$el.find which is the same as doing a jQuery find, but only in this view's context.
The movies object is our Movie Collection, instantiated with an actual title (this still needs to be done on the collection). This is because the Collection will be the one making the actual request, so it needs to have all the necessary information.
Doing a movies.fetch() will trigger the Collection's native backbone functions to preform an automatic ajax request, on a specific URL.
To trigger the getmovies function, we need to add a backbone events hash, which is an incredibly convenient way of setting up event triggers on your view. In this case we want to trigger the getmovies function when clicking the button. To add that effect, all we need to do is add this:

MovieApp.Views.Search = Backbone.View.extend({

    events: {
        'click button' : 'getmovies'
    },

    ...

That does it.

Setting up the collection

Lets setup the collection to make the request according to the parameters (or options) we gave it, in this case, we added a title parameter.

MovieApp.Collections.Movies = Backbone.Collection.extend({

    initialize: function(options){
        if (options.title)
            this.title = options.title;
    },

    url: function()
    {
        return "http://www.omdbapi.com/?s="+this.title;
    },

    parse: function(response)
    {
        return response.Search;
    }
});

The initialize function grabs the title parameter and makes it available globally inside the collection.
The url function is a native backbone function that is automatically triggered when doing the model.fetch(), in this case movies.fetch(). Whatever URL the url function returns is the endpoint one wants to request from. In this case, it's the OMDB endpoint. When doing a collection.fetch() it will take care of the ajax request for you.

The parse function is triggered after getting a successful response from the endpoint, and parses it into the Collection. The response argument is the endpoint response itself. The response from the OMDB is encapsulated inside an object, like so:

Search: [  
    0: {Title: "Batman Begins", ... }
    1: {Title: "Batman", ...}
    ...
    9: {Title: "Batman: The Dark Knight Returns, ...}

as you can see the response is not the actual list, but an array containing the list. Since backbone's Collection need to be fed the list, we need to do:

return response.Search;  

Response being the whole array, the Search parameters the list itself.

Showing the results on the page

We have made a form type thing, we read the user input, we make a request to an external API based on that input, we get the response back, now we need to show it.
So, we have a collection of movie models that we need to render. Let's setup how each movie model will be rendered, with it's own view, the one we made back in the day, the MovieView:

MovieApp.Views.MovieView = Backbone.View.extend({

    tagName: 'li',

    initialize: function(options) {

        if (options.model)
            this.model = options.model;
    },

    render: function() {

        this.$el.html(this.model.attributes.Title+" ("+this.model.attributes.Year+")");

        return this;
    }
});

We are feeding this view with a movie model parameter, much like we just fed our movies collection with a title parameter. The render will then read from that model and create it's own html.
Notice that on top of the view, there is a tagName property. This is a functionality from backbone that let's us define what type of DOM element this view will generate. Not setting up one will default to div.

Now that we have a view for a movie model, let's get those movie models from the movie collection, render their html through a movie view, and put it inside our search page.
Remember that the request is an asynchronous operation, meaning that we need to actively listen to it to trigger whatever we need to trigger when it's done, only after it's finished requesting.
Luckily we can achieve that easily by adding a success callback to movies.fetch(), much like an ajax success callback. Update your Search page's view with this:

movies.fetch({success: this.rendermovies.bind(this)});  

and the rendermovies function:

rendermovies: function(movies) {

        var movieview;

        for (var n in movies.models) {

            movieview = new MovieApp.Views.MovieView({model: movies.models[n]});

            this.$el.find('#movie-list').append(movieview.render().el);
        }
    }

Basically we are triggering the rendermovies function only after the request has finished. The callback will automatically add the request response as it's input, which in this case is a collection.
Then we just do a simple javascript cycle through all the collection's models (movie models); create a movie view for each of them; and finally append that movie view to our #movie-list div.

Hope this shines some light on how backbone things work, in a general aspect.

comments powered by Disqus