Your own sports news site with ESPN API and Knockout.js

Β· 2637 words Β· 13 minutes to read

I was really excited when I heard the announcement of the ESPN API. Being a sports freak like me, that opens a tremendous amount of possibilities for application development. Today, let’s build our own sports news single-page application, fueled entirely by Knockout.js.

You will be able to browse the latest news from ESPN from all sports categories, as well as filter them by tags. The UI will be powered by KnockoutJS and Twitter bootstrap, and yes, will be a single page. We have already done two projects together using knockout.js - last.fm API infinite scroll and ASP.NET WebAPI file upload. Hopefully we will continue our knockout.js adventures in an exciting, and interesting for you, way.

More after the jump.

ESPN API overview πŸ”—

You can find out more about ESPN API here. You will have to generate your own API key to use the API. Unfortunately, in the public portion of the API, only calls to news (headlines) and news categories are allowed. Additionally, the public API key allows only for 1 call per second and 2500 calls per day. If you want to have access to teams, players and more, or expect high volume transactions, or simply intend to use the API for commercial purposes, you’d have to reach an agreement with ESPN to become its approved partner.

Anyway, let’s get rolling.

Our UI πŸ”—

For rapid development, we will use Twitter boostrap to glue our UI. Here is what our application is going to look like (hopefully…) after we go through all development steps.

Remember this is going to be a single page application, so a lot will be happening to the UI.
You will have a menu on the left, listing all news categories. The main area (right column) will be the list of all the news. News items will be loaded in batches of 10. On the top, you will have a indicator of how many news items are being shown, and how many are available in the given category. Similarly, the same information will be available below the news section, only in the form of a progress bar (one of the functionalities perfect for Knockout.js).

Additionally, each news will display a set of tags, clicking on a tag, will filter the current news list based on a given tag.

starting off with Javascript code πŸ”—

We start off by defining an object containing all news categories available via the API. Surprisingly, ESPN API doesn’t provide a method to retrieve such collection, so we need to code it in manually.

var headline_list = {  
sport: [  
{ name: "All ESPN stories", url: "/sports" },  
{ name: "Major League Baseball (MLB)", url: "/sports/baseball/mlb" },  
{ name: "NCAA Men's College Basketball", url: "/sports/basketball/mens-college-basketball" },  
{ name: "National Basketball Association (NBA)", url: "/sports/basketball/nba" },  
{ name: "Women's National Basketball Association (WNBA)", url: "/sports/basketball/wnba" },  
{ name: "NCAA Women's College Basketball", url: "/sports/basketball/womens-college-basketball" },  
{ name: "Boxing", url: "/sports/boxing" },  
{ name: "NCAA College Football", url: "/sports/football/college-football" },  
{ name: "National Football League (NFL)", url: "/sports/football/nfl" },  
{ name: "Golf", url: "/sports/golf" },  
{ name: "National Hockey League (NHL)", url: "/sports/hockey/nhl" },  
{ name: "Horse Racing", url: "/sports/horse-racing" },  
{ name: "Auto Racing", url: "/sports/racing" },  
{ name: "Professional soccer (US focus)", url: "/sports/soccer" },  
{ name: "NASCAR Racing", url: "/sports/racing/nascar" },  
{ name: "Tennis", url: "/sports/tennis" }  
]  
};  

Next step is to define the viewModel. If you are accustomed to knockout.js, there should be no surprises. A note here is that we will have to have nested observables to facilitate our needs.

var viewModel = {  
headlines: ko.observableArray([]),  
totalHeadlines: ko.observable(0),  
categories: headline_list.sport  
selectedTag: {  
name: ko.observable(""),  
hidden: ko.observable(0)  
},  
selectedCat: {  
name: ko.observable("Welcome"),  
url: ko.observable("")  
}  
}  

Let’s go through all the properties:

  • headlines - the observable array that will contain our news items
  • totalHeadlines - total amount of news items available in the category (remember, we will be loading in batches, so we won’t load all headlines at once)
  • categories - this is just our categories list we set up manually, added to viewModel for convenience
  • selectedTag and its nested observables - if the user filters by a tag, we will store the tag name there, as well as keep track of how many news items have been fitlered out
  • selectedCat - if the user selects a category, we will story the category name and url there

These are all the properties of the viewModel. Additionally, we will need 3 viewModel methods, and 1 computed observable. I declare the separately, since the {} bracket syntax to declare JS objects doesn’t support declaring properties and methods utilizing those properties in one go.

First, let’s look at the computed observable, storyProgress:

viewModel.storyProgress = ko.computed(function () {  
if (viewModel.totalHeadlines() > 0 && viewModel.headlines().length > 0) {  
return (parseInt(viewModel.headlines().length - viewModel.selectedTag.hidden(), "10") / parseInt(viewModel.totalHeadlines(), "10")) * 100 + "%";  
} else { return 0 + "%"; }  
});  

Since it is a computed property, it’s value depends on other observables. If there are any headlines loaded, and if the total number of headlines available is greater then 0, we return a percentage value of how many of the available stories have been loaded into the headlines array. We will use that to fuel the progress bar I mentioned earlier.

Next thing is the method which will load the actual headlines into the headlines array. Let’s have a look at it.

viewModel.category_load = function (data, event) {  
viewModel.selectedCat.name(data.name);  
viewModel.selectedCat.url(data.url);  
flushCategory();  
queryESPNAPI(data.url, 10, 0);  
}  

We will be calling this method from the click event on the categories list, so we will be passing to it a data object which will be one of the objects from the previously created “headlines_list” (or, if you followed the text closely, actually the viewModel.catgories).

So whenever this method is called, we set the selectedCat to appropriate values, call the flushCategory helper method (to clear existing headlines and tags) and then query the ESPN API.

function flushCategory() {  
viewModel.headlines.removeAll();  
viewModel.selectedTag.name("");  
viewModel.selectedTag.hidden(0);  
}  

Category flushing comes down to removing all object from the headlines array and resetting the selectedTag back to default state. We will get to querying the API a bit later. For now, let’s have a look at the remaining two viewModel methods.

viewModel.load_more = function () {  
flushTagFilter();  
if (viewModel.headlines() != null && viewModel.selectedCat.url() != null) {  
var results_offset = viewModel.headlines().length;  
queryESPNAPI(viewModel.selectedCat.url(), 10, results_offset);  
}  
}  

The load_more method is called when the user wants to load the next batch of the headlines. First we need to flush the tag filtering (since the new batch will come unfiltered), then we just call the API again, but this time passing the results_offset parameter, which basically tells the API, ok we have n news items already, give me 10 more, starting from item n+1.

Finally, the last viewModel method is responsible for filtering by Tags.

viewModel.filerByTag = function (data, event, value) {  
flushTagFilter();  
viewModel.selectedTag.name(value);  
var tagname = value;  
$.each(viewModel.headlines(), function (index, headline) {  
match = false;  
if (headline.categories != null) {  
$.each(headline.categories, function (j, tag) {  
if (tag.description == tagname) {  
match = true;  
return;  
}  
});  
}  
if (!match) {  
headline.show(false);  
viewModel.selectedTag.hidden(viewModel.selectedTag.hidden() + 1);  
}  
});  
}  

This method will be called whenever the user clicks on the tag below each story. We introduce a new parameter, value, which will be passing the actual text value of the clicked element. This is necessary, since the data item is not good enough in this case( as it was for the category loading, since knockout js context item is not the Tag item, but the story item, so data contains the story item (if it doesn’t make sense, don’t worry, we’ll get back to this when we inspect our knockout.js data bindings).

The principle in Tag filtering is that, we take the tag value, iterate through all headlines, and then through all “categories” of the headline (this is how ESPN API named what we use as “tags”). If we find a match (= story has the tag we are filtering by) we exit the loop and nothing happens. If there is no match, we hide the headline, by setting its “show” observable property to false and increment the “hidden” property (which is our hidden stories counter) of viewModel.selectedTag.

For the record, let’s see what flushTagFilter method does:

function flushTagFilter() {  
$.each(viewModel.headlines(), function (index, headline) {  
headline.show(true);  
});  
viewModel.selectedTag.name("");  
viewModel.selectedTag.hidden(0);  
}

Basically, it sets all headlines back to “show” status, and clears the viewModel.selectedTag.

OK, finally, what we need to do is apply the bindings. If no category is selected (=user just arrived on the page), load the default category, which in our case are “All ESPN stories”.

ko.applyBindings(viewModel);  
if (viewModel.selectedCat.url() == "")  
viewModel.category_load(viewModel.categories[0], null);  

Querying the ESPN API and handling response πŸ”—

Now that we discussed the vieModel in detail, what we need to look at is querying the API itself. If you recall, we have encountered two calls to the API in the viewModel code, in the category_load and load_more methods.

Let’s inspect the call:

function queryESPNAPI(targeturl, limit, offset) {  
var api_call = 'http://api.espn.com/v1' + targeturl + '/news/headlines';  
$.ajax({  
url: api_call,  
dataType: 'jsonp',  
data: {  
apikey: YOUR\_API\_KEY\_GOES\_HERE,  
_accept: "application/json",  
limit: limit,  
offset: offset  
},  
success: handleESPNResponse,  
error: error  
});  
}  

The call is a pretty simple wrapper around jQuery’s AJAX GET request. We pass the category URL, which we use to build up the HTTP url, we also pass our unique API key, request limit and offset. We also define two handlers, for response and error.

Let’s look at success handler:

function handleESPNResponse(result) {  
if (result.headlines != null) {  
viewModel.totalHeadlines(result.resultsCount);  
$.each(result.headlines, function (index, headline) {  
headline.show = ko.observable(true);  
viewModel.headlines.push(headline);  
});  
}  
}  

We set the totalHeadlines property of the viewModel based on the result returned from API. Additionally, we iterate through the responses and push each headline (news item) into the headlines observable array. Before doing that, we add a boolean observable property to each headline, which we will be used for showing/hiding the story upon tag filtering (recall please the filterByTag method from earlier).

Error handler is rather self explanatory:

function error() {  
$("#error").show();  
}  

That’s almost everything, we just need a few helper handler for the UI:

$("#error a").click(function () {  
$(this).parent().hide();  
});  
$(".category a").click(function () {  
$(this).parent().parent().find(".active").each(function () {  
$(this).removeClass("active");  
});  
$(this).parent().addClass("active");  
});  
$(function () {  
$("#error").hide();  
$(".category:first").addClass("active");  
});  

They don’t do anything exciting, just add/remove CSS classes of highligthed items, or allow user to close the error box.

Off to HTML πŸ”—

Now that we have all Javascript in place, let’s shift our attention to the HTML. First we need to include all the libraries we will be relying on - and there is quite a few of them:

  • jQuery
  • Knockout.js
  • jQuery tmpl (we’ll use that instead of native knockout templates)
  • all the Twitter Boostrap extravaganza

So here is all our javascript, placed at the end of the document:

  
  
  
  
  
  

Additionally, we will load the default boostrap.css and a bit of our own custom javascript

body {  
padding-top: 60px;  
padding-bottom: 40px;  
}  
.sidebar-nav {padding: 9px 0;}  
.tagCloud span {  
float: left;  
display: block;  
margin: 0 4px 4px 0;  
}  
.tagCloud span a {color: #fff;}  

Data bindings πŸ”—

The final piece of puzzle is to write the HTML that will be bound to our viewModel and display everything as we wish.
I will not go into details of the Twitter Boostrap naming conventions and classes, you can see that yourself in the attached source code or simply at Boostrap site.
Instead, I will just focus on the actual bindings and how knockout.js will interact with the page, as I think this is much more interesting. Let’s just say that our site will be split into two columns, one span3 and one span9 and will be contained within a div row-fluid, to stretch 100% width.

Categories list
Categories list contains clickable news sections.


<div class="span3">
  <div class="well sidebar-nav">
    <ul class="nav nav-list">
      <li class="nav-header">
        Categories
      </li>
    </ul>
    
    <ul data-bind="template: { name : 'categoriesTemplate', foreach: categories }" class="nav nav-list">
    </ul></p>
  </div>
</div>

For simplicity, we use two separate ul-s (not the ebst practice). Since Twitter Boostrap uses 1st li as a header, I want to avoid having to inject a dummy item on top of the categories list binding. Categories list is bound to the categoriesTemplate, and iterates through viewModel.categories.

  

Categories template displayes category name, and upon clicking calles the category_load method. The data bound item is passed to the method as data object, which was explained earlier when we discussed the category_load method.

Content pane
Content column consists of three sections:

  • top: where we display category name, item count and current tag used for filtering
  • middle: where we display headline items + tags relevant for each
  • bottom: where we display progress bar and “load more” button
  1. Top section:

<div class="page-header">
  <h1>
    <span data-bind="text: selectedCat.name">Page Title</span> <small>Showing <small data-bind="text: headlines().length - selectedTag.hidden()"></small> out of <small data-bind="text: totalHeadlines"></small> stories.</small>
  </h1>
</div><div class="row-fluid" data-bind="visible: selectedTag.name().length > 0&#8243;></p> 

<div>
  Currently filtering by: <span class="label label-success" data-bind="text: selectedTag.name"></span>
</div></div> 

So we bind page title to viewModel.selectedCat.name property. We display information of how many items are shown by substracting the items hidden by tag filtering (so without filtering, 0) from the total number of headlines loaded. Additionally we display and information of how many total headlines are available.

We also add a binding for tag filtering indicator. It’s visibility is tied to the fact if selectedTag.name property is set or not. If it’s not, nothing is shown, if it is, then we display the selectedTag.name property.

  1. Middle area, where the headlines are displayed

<div data-bind="template: { name : 'headlineTemplate', foreach: viewModel.headlines }">
</div>

The binding is super simply, just binding to the headlines observable array. More interesting stuff in the template that’s used there:

  

First we wrap everything in an {{if show}} condition, meaning that if the property of headline object is false, nothing is displayed. This is our tag filtering prerequisite. the we display the new story “headline” propert (= title), link to ESPN and the description. Then we also show the story type and published date. Finally we display the tags (again, confusing enough since ESPN API calls them “categories”). We loop through all categories (=tags) using {{each}} and we add a click handler to each tag, binding it to filterByTag method. As mentioned, due to a shift in context, data parameter is still the parent (headline), we need to manually pass the value of the tag by using $value.description.

  1. Bottom section
    Finally, at the bottom sits the stories progress bar (which will tell the user how many out of all the stories have been loaded) and a a button to load more.

<div class="progress progress-striped active">
  <div class="bar" data-bind="style: {width: storyProgress}">
  </div></p>
</div>

<a class="btn btn-primary btn-large" data-bind="click: load_more" href="javascript:return false;">Load more</a>  

The progress bar is a Twitter Boostrap functionality, all we need to pass to it is the percentage value, which we generate using our computed property of the viewModel, sttoryProgress. And the “load more” button is just bound to viewModel.load_more event.

Tryin it out πŸ”—

So now that everything is in place, all that’s left to do is to try the web application out. This is how it should look after you load:

Clicking on any category, loads that category - i.e. NHL

Filtering by tag:

Notice how nicely the progress bat reacts to all changes in the headlines array:

Source code and demo πŸ”—

As usually, you can download the source code. Please remember to generate your own API key and insert it in the the queryESPNAPI method of the code! Otherwise the source code will not work. you can generate your API key here.

Additionally, please note that the demo may not work perfectly fine under heavy load - remember that public ESPN API key allows for only 1 call per second and 2500 calls per day.

Thanks guys & gals, till next time.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP