Consuming an API is a Breeze

Having trouble getting your new JavaScript libraries to play together nicely?  Want to see how you can use Breeze.js and Knockout.js to consume any API, ever, since the history of your browser?  Then look no further…

Why?

Checking out new JavaScript libraries and Frameworks can be scary.  Do you have a back-end already in place and just want to see what you can do on the front-end with Breeze.js and Knockout.js?  This sample / walk-thru is served only with an API, ESPN’s Developer API to be specific.  The only thing we do on the server here is server up our view from a Durandal.js starter app.  I choose Durandal.js because it is an extremely fast way to get a new project up in running, and provides a working app that needs only a few minutes to transform into an app of your own.

You can grab a working sample here of the finished code – http://github.com/PWKad/BreezeAPIOnlyKO (Note: this project uses NuGet package restore – read more about it here if you are unfamiliar http://docs.nuget.org/docs/workflows/using-nuget-without-committing-packages)

Breeze.js is an open-source JavaScript library from the established and well-respected team at IdeaBlade aimed at providing rich client-side data to your JavaScript apps.

  • Handles caching your data in the browser
  • Keeps track of changes to your JavaScript entities (and supports canceling those changes easily!)
  • Provides Linq-style queries in JavaScript

Knockout.js is an open-source JavaScript library created by Steve Sanderson to provide data binding all the way back to IE 6 (!!!!!)

  • Utilizes the MVVM pattern
  • Is an easy bolt-on to any project (Knockout is only concerned with binding data and keeping your view up-to-date)
  • Plug-ins are available to tackle any problem you have, and scale easily

Summary – 

The objective of this walk-thru is to help new to intermediate level JavaScript developers understand Breeze.js better and get a glimpse of the functionality when coupled with Knockout.js.  We will create a new project, add basic dependencies, set up Breeze to use with any API, create a JSON results adapter to map complex data structures, and bind our data to the view.

This is an open-source sample so if you see a problem feel free to make a pull request.  If you see a typo or something that just isn’t right feel free to leave a comment, or even send me a message either on here or on GitHub – pw kad

Technologies used –

Breeze.js – http://www.breezejs.com/

Knockout.js – http://knockoutjs.com/

Durandal.js – http://durandaljs.com/ – (Use the starter kit to get a JavaScript app up and running extremely fast)

Twitter Bootstrap – http://getbootstrap.com/2.3.2/

Getting Started – 

As a note, I use Visual Studio 2012 as my text editor for familiarity and ease-of-use. Let me know if you see any errors or have any helpful hints in the comments.

Create our new project – 

The Durandal Starter Kit is available at http://durandaljs.com/pages/downloads/ as a free download and also via http://www.asp.net/single-page-application/overview/templates/durandaljs-template as the SPA template (Note: Durandal 2.0 is now released, this project currently uses Durandal 1.2)

Open VS2012 and create a new ASP.NET MVC 4 application.  You can either create an Empty project and use NuGet to get the Durandal Starter Kit or install the template listed above and use Durandal SPA Project from the available templates window.

Creating a Durandal Starter Kit project

Creating a Durandal Starter Kit project’

We need to add a directory to our ‘App’ folder for the services we will use – App > services - This is where we will place our JavaScript files for getting data, creating the models, and any other JS files that are not view models.

Adding dependencies – 

Right click on our newly created project and choose Manage nuget packages… this will allow us to add our dependencies quickly and easily.

Choose the online option on the left of the pop-up to search online for available packages and in the top right search box type breeze to search for Breeze.js.  Install the Breeze for ASP.NET Web Api Projects package.  This package will add Breeze.js to the scripts folder, but it will also add some server-side stuff that we won’t be using.  (Basically if we were creating this project to serve up data as well as consume it we would need this additional library to make life easier, but since we will only be consuming the API we don’t need to worry about it)

Feel free to take a look in the scripts folder and check out our new dependencies.  Q.js is a promises library that Breeze uses to perform Asynchronous operations, such as querying.  If Sammy.js is in there, it was provided by our Durandal Starter Kit to help with routing and navigation (Note: Durandal 2.0 does not use the router plug-in including Sammy.js for routing nor navigation)

The only thing in the Controllers directory should be our Durandal controller, which will serve up our initial view.  The only thing in the Views folder are the splash page Durandal uses when loading up and an Index.cshtml file to host our Single Page App on.

Adding services – 

Inside of our services directory we need to add three JavaScript files (modules) –

First, let’s add our datacontext.js to the App/services folder.  This is requiring two services we haven’t finished setting up yet, (model and jsonResultsAdapter) so this won’t yet compile properly.

define(['services/model', 'services/jsonResultsAdapter'],
    function (model, jsonResultsAdapter) {

    });

Note on AMD (Asynchronous Module Definition) –

The define() statement defines the beginning of a module and is used to inject dependencies.  In this case we are using system/durandal and log system messages, so we require it and let our module know it can call it by using system

We will be using the model and jsonResultsAdapter in this module so we need to require them. Let’s add some basic functions to our datacontext

        var EntityQuery = breeze.EntityQuery;
        function returnResults(data) {
            return data.results;
        }
        var datacontext = {
        };
        return datacontext;
        function getLocal(resource, entityType, ordering) {
            var query = EntityQuery.from(resource)
                .toType(entityType)
                .orderBy(ordering);
            return manager.executeQueryLocally(query);
        }
        function queryFailed(error) {
            var msg = 'Error retrieving data. ' + error.message;
            console.log(msg);
        }

The EntityQuery variable will negate our need to call breeze.EntityQuery for each query we write.

The datacontext object we are creating is used to expose our functions to other modules that require it when it is returned.

getLocal is just a helper function to get local instances of an entity.

queryFailed is just a helper function logging errors in the console.

Setting up Breeze in our DataContext – 

We need to set up Breeze inside our datacontext

        var serviceName = "http://api.espn.com/v1/sports/baseball/mlb";
        var ds = new breeze.DataService({
            serviceName: serviceName,
            hasServerMetadata: false,
            useJsonp: true,
            jsonResultsAdapter: jsonResultsAdapter
        });
        function configureBreezeManager() {
            var mgr = new breeze.EntityManager({ dataService: ds });
            return mgr;
        }
        var manager = configureBreezeManager();
        model.initialize(manager.metadataStore);
        var metadataStore = manager.metadataStore;

This sets up a data-service, letting Breeze know that we don’t have any meta data available and will have to make our own and also let’s Breeze know we are going to use a custom jsonResultsAdapter.  We haven’t yet created it, but it is being required at the top of our datacontext in our define statement so it is available to us inside of it.

Next we need to create a model.js in our App/services folder –

define(['configuration'], function (config) {
    var DT = breeze.DataType;
    var model = {
        initialize: initialize
    };
    return model;
});

As you can see, we are revealing a function called initialize that initializes our entity models. We could have put this into our datacontext module but in the interest of the separation of concerns principle we want to keep this module separate.

    function initialize(metadataStore) {
        metadataStore.addEntityType({
            shortName: "NewsItem",
            namespace: "ESPN",
            dataProperties: {
                id: { dataType: "Int64", isPartOfKey: true },
                teamId: { dataType: "Int64" },
                headline: { dataType: "String" },
                description: { dataType: "String" },
                source: { dataType: "String" },
                imageSource: { dataType: "String" },
                imageCaption: { dataType: "String" },
                imageCredit: { dataType: "String" },
                link: { dataType: "String" }
            },
            navigationProperties: {
                team: {
                    entityTypeName: "Team", isScalar: true,
                    associationName: "Team_NewsItems", foreignKeyNames: ["teamId"]
                }
            }
        });
        metadataStore.addEntityType({
            shortName: "Team",
            namespace: "ESPN",
            dataProperties: {
                id: { dataType: "Int64", isPartOfKey: true },
                location: { dataType: "String" },
                name: { dataType: "String" },
                abbreviation: { dataType: "String" },
                color: { dataType: "String" }
            },
            navigationProperties: {
                newsItems: {
                    entityTypeName: "NewsItem", isScalar: false,
                    associationName: "Team_NewsItems"
                }
            }
        });
        metadataStore.registerEntityTypeCtor(
            'Team', null, teamInitializer);
        function teamInitializer(team) {
            team.fullName = ko.computed(function () {
                var loc = team.location();
                var name = team.name();
                return loc + ' ' + name;
            });
            team.showNews = ko.observable(false);
        }
    }

If you remember earlier in our datacontext we passed the metadataStore into the model.initialize() function. model.initialize(manager.metadataStore);. In the model we are adding types ‘Team’ and ‘NewsItem’. These entities have navigation properties linking them, which we set up with an association. This will allow us to easily bind the data in the view, and will remove the need to make additional calls to the server.

Finally, we want to register a custom property on the Team called ‘fullName’ which is a computed observable. We will use this in the view to represent a team’s location and name (ie. Texas Rangers) We are also setting an observable called showNews that we will use to flag whether the team’s news is being shown. Every time an entity is pulled in from the server or created these functions will be executed.

Now let’s set up our API calls in the datacontext

        var myAPIKEY = "qubdkem5nhuctxtxghkx32nm";
        var getTeams = function (teamsObservable, forceRemote) {
            if (!forceRemote) {
                var p = getLocal('Teams', 'Team', 'id');
                if (p.length > 0) {
                    teamsObservable(p);
                    return Q.resolve();
                }
            }
            var parameters = makeParameters();
            var query = breeze.EntityQuery
                .from("teams")
                .withParameters(parameters)
                .toType('Team');
            return manager.executeQuery(query).then(querySucceeded).fail(queryFailed);
            function querySucceeded(data) {
                var s = data.results;
                return teamsObservable(s);
            }
        };
        var getTeamNews = function (team) {            
            var parameters = makeParameters();
            var query = breeze.EntityQuery
                .from("teams/" + team.id() + "/news")
                .withParameters(parameters);
            return manager.executeQuery(query).then(querySucceeded).fail(queryFailed);
            function querySucceeded(data) {
                var s = data.results;
                var tempObs = ko.observableArray(s);
                // Since the news item has multiple categories and can be for multiple teams
                // we will set the team explicitly to the team we are searching for.
                // We could set it to each team, but this is a simple example.
                ko.utils.arrayForEach(tempObs(), function (newsitem) {
                    newsitem.teamId(team.id());
                });
                return true;
            }
        };
        function makeParameters(addlParameters) {
            var parameters = {
                apikey: myAPIKEY
            };
            return breeze.core.extend(parameters, addlParameters);
        }
        function returnResults(data) {
            return data.results;
        }

There is a lot going on here so let’s get down into more detail –

myAPIKEY is an API key provided by ESPN.  You can register for your own at http://developer.espn.com/

getTeams and getTeamNews are two functions that perform Breeze EntityQuery’s.  You can learn a lot more about structuring these queries on Breeze’s website, and I won’t cover exactly how they work here (would make this walk-thru much longer) but understand that these queries are structured to check the local cache for entities, and if there are none they will go hit the API.

makeParameters is an internal helper function that creates parameters.  The ESPN developer API requires a key to be passed, and we are extending any additional parameters passed in.  This will make our API call look something like this –

http://api.espn.com/v1/sports/baseball/mlb/teams/2/news?apikey=[apiKey will show up here]

Finally, there is a returnResults function that we will use to return the data.results from the callback.

We need to expose those functions to any other module that is requiring the datacontext, so adjust the object we are returning in our datacontext.

        var datacontext = {
            getTeams: getTeams,
            getTeamNews: getTeamNews
        };
        return datacontext;

Now that our datacontext is ready to go, we need to set up our jsonResultsAdapter.js

define([], new breeze.JsonResultsAdapter({
    name: "ESPN",
    extractResults: function (data) {
        var results = data.results;
        if (!results) throw new Error("Unable to resolve 'results' property");
        return results && (results.headlines);
    },
    visitNode: function (node, mappingContext, nodeContext) {
        if (node.headline) {
            if (node.images.length > 0) { 
                node.imageSource = node.images[0].url;
                node.imageCaption = node.images[0].caption;
                node.imageCredit = node.images[0].credit;
            }
            else {
                node.imageSource = '../content/images/blank_image.png';
                node.imageCaption = 'none';
                node.imageCredit = 'no credit';
            }
            node.link = node.links.web.href;
            return { entityType: "NewsItem" };
        }
    }
}));

This is a custom adapter we register in our datacontext to map the results from a complex JSON structure.  You can read more about how it works in the Breeze docs, but here is a basic breakdown –

name assigns a namespace.  If you remember earlier when we created the entity types in our model we assigned them to a namespace.

extractResults makes sure that the data returned has a results property and then checks to see if there is a headlines property.  If so, we create entities out of the results.  The reason we check for a headlines is because it is a unique property on the news objects being returned, so we know that if a result has a property headlines it should be mapped to a NewsItem.  When a news item is created we also need to map some of it’s properties.

Alright, that was a lot of JavaScript.  Let’s see some results –

Note – The below portion of this walk-thru doesn’t go into a lot of detail yet.  Give me a few days and I can explain it in more detail.

Delete the code inside your home.html in your App/views folder. Replace it with the below HTML –

<h3 class="teams-header">Team News - <img src="http://a.espncdn.com/i/apis/attribution/espn-api-black_150.png" /></h3>
<div class="accordion" id="sports-accordion" data-bind="foreach: teams">
  <div class="accordion-group">
    <div class="accordion-heading" data-bind="style: { backgroundColor: '#' + color() }" >
        <a class="accordion-toggle team-brief" data-toggle="collapse" data-parent="#sports-accordion" 
            data-bind="attr: { href: '#' + abbreviation() }" >
            <p><span data-bind="text: abbreviation"></span> - <span data-bind="text: fullName"></span></p>
        </a>
    </div>
    <!-- ko if: showNews() === true -->
    <h5 data-bind="visible: newsItems().length === 0">Fetching news...</h5>
    <div data-bind="attr: { id: abbreviation }" class="accordion-body collapse in">
      <div class="accordion-inner">
        <ul class="media-list" data-bind="foreach: newsItems">
            <li class="media">
                <a class="pull-left" data-bind="attr: { href: link }">
                    <img class="media-object" data-bind="attr: { src: imageSource(), title: imageCredit }" />
                </a>
                <div class="media-body">
                    <h4 class="media-heading" data-bind="text: headline"></h4>
                    <blockquote>
                        <p data-bind="text: description"></p>
                        <small>from <cite title="Source" data-bind="text: source"></cite></small>
                    </blockquote>
                </div>
            </li>
        </ul>
      </div>
    </div>
    <!-- /ko -->
  </div>
</div>

This is a Twitter Bootstrap accordion bound to our Knockout data.

Now replace all of the home.js code in our App/viewmodels folder with the below –

define(['services/datacontext', 'viewmodels/shell'], function (datacontext, shell) {
    var teams = ko.observableArray();
    var bindEventToList = function (rootSelector, selector, callback, eventName) {
        var eName = eventName || 'click';
        $(rootSelector).on(eName, selector, function () {
            var team = ko.dataFor(this);
            callback(team);
            return false;
        });
    };    
    var toggleTeamNews = function (team) {
        if (team.showNews() === true) { team.showNews(false); }
        else {
            if (team.newsItems().length === 0) {
                datacontext.getTeamNews(team).fail(queryFailed);
            }
            team.showNews(true);
        }
    };
    var viewAttached = function (view) {
        bindEventToList(view, '.team-brief', toggleTeamNews);
    };
    var initLookups = function () {
        datacontext.getTeams(teams).fail(queryFailed);
    };
    function queryFailed(error) {
        console.log(error.message + " - Query failed; please try it again.");
    }
    var activate = function () {
        initLookups();
        return true;
    };
    var home = {
        activate: activate,
        teams: teams,
        viewAttached: viewAttached,
        shell: shell
    };
    return home;
});

That’s it! Run our app and you will see a list of MLB teams, with the names on top of their team color returned from the server. Clicking on any of the teams will go fetch their news, and once we have fetched it Breeze is caching the NewsItem’s so we can hide or display them as we see fit, without having to hit the server again.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s