KnockoutJS Flickr App Tutorial – Part I
To better understand how to implementation of KnockoutJS, I have created a tutorial which creates a Flickr application searching Flickr photos based on tag and displaying them. It also allows the user to view an enlarged version of the photo and add it to a favorites section. Here is Balsamiq mockup of the application:
Model
In this example the Model will contains the information/data for the application. In this case we have two types of data, Photo and Favorite. The Photo model will have the following structure:
Photo = function(id, owner, title, farmId, serverId, secret, ispublic, isfriend, isfamily) { this.id = id; this.owner = owner; this.title = title; this.farmId = farmId; this.serverId = serverId; this.secret = secret; this.ispublic = ispublic; this.isfriend = isfriend; this.isfamily = isfamily; };
The Favorite Model is simply a collection of Photo models:
Favorites = function(Photo) { return this.Photo = Photo; };
ViewModel
The ViewModel not contains the objects that will be used in the view but also the functions and interactions between the model and the view. The first step is to create observable objects. KnockoutJS has three types of observable objects:
- Simple object
- Function
- Array Object
Here are some how the observables will be used in KnockoutJS:
// Simple object var myViewModel = { personName: ko.observable('Bob'), personAge: ko.observable(123) }; //Function function AppViewModel() { this.firstName = ko.observable('Bob'); this.lastName = ko.observable('Smith'); this.fullName = ko.computed(function() { return this.firstName() + " " + this.lastName(); }, this); }; //Array var myObservableArray = ko.observableArray(); // Initially an empty array myObservableArray.push('Some value'); // Adds the value and notifies observers
Keep in mind that some browser do not support getters and setters so all ko.observable objects are functions In order retrieve a set value, one will have to use the following code:
myViewModel.personName(); //will display Bob
To change the value of a viewModel, you will implement in the following manner:
myViewModel.personName('Roy'); myViewModel.personName(); //will display Roy
In order to create the proper structure, the first step is the create the objects of the viewModel. Some of the time, these objects will mirror the model data that has been returned from the server. However there will be times where we will need to create an object reflecting not only the information displayed to the user but also to record the interactions with the user. In our application, we have created a photos object which contains arrays of ‘image’ objects. The ‘image’ object has the following structure:
- Photo object: this is an instance of the Photo object created in model
- Photo src: this is the source for our image to be retrieved from Flickr
photoViewModel = { //Photos: will be an array of image objects which will contain //Photo object and photo src photos: ko.observableArray([]), //Favorites favorites: ko.observableArray([]) }
As you can see, the ‘photos’ and ‘favorites’ objects are initialized as empty knockout observable arrays. This is all that is needed in order to initially render the page. Once the user searches for a certain tag i.e. cat, dog, table, etc.., we will fire off a request to Flickr to retrieve the images. In the viewModel, we also create functions
photoViewModel = { ... //Photos and Favorites object are here //Function that will retrieve all the photos based on the search tag //and place them in the photos viewmodel getPhotos: function(formElement) { var searchTag, that; //reseting the previous search results photoViewModel.photos([]); //getting the search tag searchTag = $(formElement).find('#search').val(); that = this; $.getJSON(flickrURL + searchTag) .done(function(photoData) { return _.each(photoData.photos.photo, function(photo) { var image; image = { photoObj: new Photo(photo.id, photo.owner, photo.title, photo.farm, photo.server, photo.secret, photo.ispublic, photo.isfriend, photo.isfamily), photoSrc: that.createSrc(photo.farm, photo.server, photo.id, photo.secret, 'thumbnail') }; return that.photos.push(image); }); }); } }
We also need a function that will create the src URL for the images for us.
//Function that will create a src URL to be retrieved from Flickr</pre> createSrc: function(farmId, serverId, id, secret, size) { return 'http://farm' + farmId + '.staticflickr.com/' + serverId + '/' + id + '_' + secret + (size === "thumbnail" ? "_s.jpg" : "_n.jpg"); }
To add a photo to the favorites, we simply take the photo object that the user has selected and add it to the favorites array. By simply adding the object to the array combined with the fact that the favorites object is bound to the DOM element on the view, the photo will appear in the designated area.
//Function that will add the selected photo to the favorites viewmodel addToFavorites: function(id) { var currentPhotoObj, favoriteExists; //getting the photo object of the selected photo currentPhotoObj = _.find(photoViewModel.photos(), function(photo) { return photo.photoObj['id'] === id; }); //Checking to see if the item exists in the favorites or not favoriteExists = _.find(photoViewModel.favorites(), function(favorite) { return favorite.photoObj['id'] === currentPhotoObj.photoObj.id; }); //adding the photo object to the favorites list if (!favoriteExists) { photoViewModel.favorites.push(currentPhotoObj); } },
The remaining code will perform the following functions:
- remove from favorites
- Show larger image
//Function that will remove the selected photo from the favorites viewmodel removeFromFavorites: function(id) { return photoViewModel.favorites.splice(_.indexOf(photoViewModel.favorites(), _.find(photoViewModel.favorites(), function(favorite) { return favorite.photoObj['id'] === id; })), 1); }, //Function that will show the larger image in the a modal box showLargerImage: function(id) { var currentPhotoObj, imageSrc; currentPhotoObj = _.find(photoViewModel.photos(), function(photo) { return photo.photoObj['id'] === id; }); imageSrc = photoViewModel.createSrc(currentPhotoObj.photoObj.farmId, currentPhotoObj.photoObj.serverId, currentPhotoObj.photoObj.id, currentPhotoObj.photoObj.secret, 'large'); $('body').modal({ height: 'auto', width: 'auto', loaderImgSrc: 'img/loader.gif', showSpinner: false }); $('body').modal('openModal'); return $('body').modal('updateContent', $('</pre> <img alt="" />').attr('src', imageSrc)); } };
Note that in this process, we are using underscore.js and jquery.modal.js (a jquery plugin that I wrote myself).
View
The view being the center where we display the model at its current state to the user and also give the user the ability to modify/update the model. This is the markup for the search box:
<header> <h1>Photo Finder</h1> <strong>Search for photos in Flicker using tags</strong><form data-bind="submit: getPhotos"> <input id="search" type="text" /> </form> </header>
As you can see, we have bound the submit button to the ‘getPhotos’ function in our view model. This binding (for the submit button binding ensure that the button is in a form) will prevent the default submit action but also will allow you to use the form as a handler for the view model and perform the specific function needed. In our case, we use it to retrieve the Flickr photos and place them in our view model. Once the photos are retrieved, we add them to the photos object in the view model. Because the view model is bound to the DOM, therefore consumed by the ‘photos-template’. At this point, any changes made to the photos object in the view model will be reflected in the UI automagically.
</pre> <div id="main"> <h4>Search Results</h4> <hr width="81%" /> <div id="results" data-bind="template: 'photos-template'"></div> ...<script id="photos-template" type="text/html">// <![CDATA[ <ul class="clearfix"> {{each photos}} <li> <div class="imgResult"><img src="${photoSrc}" id="${photoObj.id}" alt="${photoObj.title}- ${photoObj.owner}" title="${photoObj.title}" /></div> <div class="tabs"><img src="img/fav-icon-sketch.png" width="40%" alt="add to favorites" data-bind="click: function() { addToFavorites(photoObj.id) }"/> <img src="img/enlarge-sketch.png" width="40%" alt="show larger image" data-bind="click: function() { showLargerImage(photoObj.id) }"/></div></li> {{/each}}</ul> // ]]></script>
Also please notice that we have not only bound the photos object to this markup/template but we have also bound a couple of ‘click’ events that will call the appropriate function, passing the the photo id. Similar treatment for the favorites section:
</pre> <div id="favs"> <div class="header">Favorites<img src="img/fav-icon-sketch.png" alt="add to favorites" width="15%" /></div> <div id="favorites" data-bind="template: 'favorites-template'"></div> </div> <h3>...<script id="favorites-template" type="text/html">// <![CDATA[ <ul class="clearfix"> {{each favorites}} <li> <div class="imgResult"><img src="${photoSrc}" id="${photoObj.id}" alt="${photoObj.title}-${photoObj.owner}" title="${photoObj.title}" /></div> <div class="tabs"><img src="img/remove-sketch.png" width="40%" alt="add to favorites" data-bind="click: function() { removeFromFavorites(photoObj.id) }"/></div></li> {{/each}}</ul> // ]]></script>
Reference
Next Steps
Common complaint of KnockoutJS is the what seems like the mixing of javascript, data-bind attributes, and markup. However KnockoutJS allows you to create custom bindings. One option to combat the overly tied application logic to the view is to create a custom binding similar to CSS classes. Ryan Niemeyer has suggested this procedure. In part II of this tutorial, we will tackle this option.