Build Facebook style infinite scroll with knockout.js and Last.fm API

Β· 1484 words Β· 7 minutes to read

I’m convinced that most of us web developers often have to struggle with some sophisticated Javascript driven UI that tend to get out hand with their complexity, dependencies and relationships.

With that in mind, I wanted to show you an example of a knockout.js code in action.

Since knockout.js is one of the most amazing and innovative pieces of front-end code I have seen in recent years, I hope this is going to help you a bit in your everday battles. In conjuction with Last.FM API, we are going to create an infinitely scrollable history of your music records - just like the infinite scroll used on Facebook or on Twitter.

More after the jump.

What is it all about? πŸ”—

So what exatcly will we be trying to achieve here?

    • display all the tracks that a user scrobbled to last.fm
    • load them in batches - load next batch automagically as soon as user scrolls to the bottom of the page
    • provide a possibility to change user data

ViewModel - the heart and soul πŸ”—

Alright, so let’s go straight to business. I will not go into the very basics of knockout.js, as the basics are very well described on the knockout.js site. I’ll just say that knockout.js provides the possibility to set observables and observableArrays on the viewModel. And if you are still feeling confused, well, the best way to learn is to follow the example.

Our model will look like this:

var viewModel = {  
tracks: ko.observableArray([]),  
totalTracks: ko.observable(0),  
page: ko.observable(0),  
username: ko.observable("littleidsdog")  
}  

Let’s spend a minute and look at it. The tracks property is going to hold the tracks retrieved from last.fm. totalTracks is going to hold the value for all tracks that the user has ever listened to. This is going to be useful to tell the page to stop trying to load new tracks when we reach the end of available data. Page property is going to be used for indicating on which page (or batch) of the last.fm results we are currently on. Each new page will be corresponding to a new $.ajax (XHR) call. username is rather self explanatory (I hope).

Now that we have tha basic viewModel planned, we need to extend it with a changing the user functionality. But before that, let’s think of how we might read info from Last.fm using its API.

Last.fm API giveth and not taketh away πŸ”—

The Last.fm API is documented here. It is a fantastic resource, with a wide variety of methods. In this sample we’ll use just one - user.getrecenttracks, which contrary to what the name suggests, allows us to retrieve all the tracks user has ever listened to - from the latest to the oldest one.

Last.fm API is available in both JSON and XML format. You’ll need a free API key to communicate with the API, but in this tutorial we will get a public key (stolen from last.fm API documentation ;-).
We’ll use the function below to read data from the API.

function getFromLastFM(username, methodname, page) {  
var api_url = 'http://ws.audioscrobbler.com/2.0/';  
$.ajax({  
url: api_url,  
dataType: 'jsonp',  
data: {  
user: username,  
method: methodname,  
api_key: "b25b959554ed76058ac220b7b2e0a026",  
format: "json",  
page: page,  
limit: 20  
},  
beforeSend: function() {  
$("#ajaxload").show();  
},  
success: function (result) {  
if (result.recenttracks != null) {  
viewModel.totalTracks(result.recenttracks["@attr"].total);  
viewModel.page(result.recenttracks["@attr"].page);  
$("#ajaxload").hide();  
$.each(result.recenttracks.track, function (index, tr) {  
viewModel.tracks.push(tr);  
});  
}  
},  
error: function () {  
$("#error").show();  
}  
});  
}  

As you see, the request is rather simple, we pass the methodname (in our case it’s always going to be “user.getrecentracks”), username, api_key, page (since as I already mentioned, results returned by last.fm are paged, or batched) and a limit (amount of tracks within a page).

The majority of interesting things happen in the processing of the response. First of all, we set the viewModel’s totalTracks and page properties based on the data returned. Then we iterate through the list of tracks, and push the last.fm track object into the observableArray - viewModel.tracks. At this point in time you can probably imagine that these observables are going to do some heaving lifting for us. Also, notice that we add tracks by using push() method, which means we keep adding tracks to the already loaded and shown ones.

OK, now that we have defined the function to be used to call last.fm API, we need to extend the viewModel with the aforementioned support for switching user.

viewModel.reloadUser = function (data, event) {  
viewModel.username($("#username").val());  
viewModel.tracks.removeAll();  
getFromLastFM(viewModel.username, "user.getrecenttracks");  
};  

The method reads the user value from a textbox, clears the existing tracks collection, and starts loading data for the new user. The reason why we declare it as viewModel method, rather then a standalone function is that we will use knockout.js to bind it to click event.

Infinite scroll is not infinite headache πŸ”—

Now let’s implement the final piece of our puzzle, the infinite scroll. As complex as it sounds, it’s actually very simple.

$(window).scroll(function () {  
if ($(window).scrollTop() == $(document).height() - $(window).height()) {  
if (parseInt(viewModel.page(), "10") * 20 < parseInt(viewModel.totalTracks(), "10")) { getFromLastFM(viewModel.username, "user.getrecenttracks", parseInt(viewModel.page(), "10") + 1); } } });

We bind an anoymous function to the scroll event of the window object, and if we reach the bottom of the page (calculated based on scrollTop http://api.jquery.com/scrollTop/), we check whether we have reached the end of users tracks yet (expressed by page number * page size - in our case 20 - and compared to total tracks on user’s last.fm profile). If there is still stuff to load, we call our last.fm retrieval method, this time pasing page+1 as an argument, since we are interested in the next batch. getFromLastFM(viewModel.username, "user.getrecenttracks", parseInt(viewModel.page(), "10") + 1);

Where the heck is the View? πŸ”—

Now pretty much all the JS is in place, except for binding to the View and the initial population of data, but that in a minute. For now let’s shift our attention to the View. So we have this nice viewModel ready to be bound to the UI, which we have completely disregarded up to this point. Well, actually turns out this is going to be super easy to do. The way knockout.js is built, once you have the viewModel, the view can be set up (almost) in a heartbeat.

Our main data (tracks) will be included in a div, and we’ll loop through the viewModel.tracks to render all available tracks.

<div data-bind="template: { name : 'trackTemplate', foreach: tracks }">
</div>

The HTML5 data-bind attribute, is a way for us to tell knockout.js “hey there is something for you to look at”. In this case we are saying “loop through all of viewModel.tracks objects, and render each in the context of trackTemplate”. But where is that template? Here:

  

If you are familiar with jQuery templates, there is nothing surprising here, all properties are determined by the individual track object returned by last.fm API. There is not much to explain here, except for maybe mentioning that some of the properties returned by last.fm are named with “#” at the beginning, and thus are not accessible using the normal “dot"syntax (object.property), so we have to use the brackets (or magic strings) syntax (object[“property”]) instead.

New user and some side effects πŸ”—

Now let’s just a add a little header with username, and a textbox to enter new username.

<div class="page-header">
  <h1>
    <span data-bind="text: username">Page Title</span><br /> <small>Showing<br /> <small data-bind="text: tracks().length "></small><br /> out of <small data-bind="text: totalTracks"></small><br /> tracks.</small>
  </h1>
  
  <div>
    <em>Your username goes in here: </em><br /> <input id="username" type="text" data-bind="value: username" /><br /> <a href="javascript:false;" data-bind="click: reloadUser">Click me</a>
  </div>
</div>

We bind a click event to our reloadUser method of viewModel (as explained later). We also added two bindings for showing how many tracks have been loaded so far (by doing a count on the viewModel.tracks observableArray) and how many tracks the user has in total.

Putting it all together and hoping it works πŸ”—

So the view is pretty much ready - let’s bind the two together.

ko.applyBindings(viewModel);  
getFromLastFM(viewModel.username, "user.getrecenttracks");  

We tell knockout.js to apply the bindings by calling, well, surprisingly, applyBindings method and passing the viewModel to it. We also call the method to get tracks from last.fm once, to populate the page on 1st load.

By that time the page should look somehow like this:

It works but it’s nothing exciting. Well, at this point it’s just a matter to add a bit of CSS.

body {  
margin: 20px;  
font-family: Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif;}  
h1 {font-size: 2em;}  
h3 {font-size: 1.4em;margin: 0 0 10px 0;}  
small {font-size: 0.4em; color: #808080;}  
i {font-size: 0.9em;}  
h1 small small {font-size: 1em;}  
.track {padding: 10px; background: #00bc03; margin: 10px;}  
.track.history {background: #e1dbdb;}  
img {float: left;margin-right: 10px;}  
p {font-size: 0.8em;display: block;}  
p.album {font-size: 0.7em;font-style: italic;}  
p small {font-size: 0.8em;}  
#ajaxload {  
height:32px;  
width: 32px;  
display: none;  
float: left;  
display: inline-block;  
}  
a {font-size: 0.8em;text-decoration: none;color:#00BC03;}  
a:hover {text-decoration: underline;}  

And you should now get to something like this.

And that’s about it.

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