Advertisement
JavaScript & AJAX

Building a Web App From Scratch in AngularJS

by

In a previous AngularJS tutorial I covered all the basics of how to get up and running with Angular in around 30 minutes. This tutorial will expand on what was covered there by creating a simple real world web application.

This simple web application will allow its users to view, search and filter TV Show Premieres for the next 30 days. As a keen series viewer, I am always looking for something new to watch when my favorite shows are off air, so I thought I would create an app to help me find what I am looking for.

Before we get started, you may want to take a look at the demo from above, to see what we will be creating in this tutorial.


Getting Started

To begin, we need a skeleton AngularJS application which already has all the required JavaScript and CSS to create the TV Show Premieres app. Go ahead and download this skeleton from the "download source files" button above.

Once you have downloaded the files you should have a directory structure as shown below:

figure1-skeleton-directory-structure

Looking at the directory structure and the included files you will see that we will be using Twitter Bootstrap to make our web app a little prettier, but this tutorial will not look at Twitter Bootstrap in any detail (learn more about Twitter Bootstrap). Additionally, this tutorial will not be covering how to setup a new AngularJS application as the aforementioned AngularJS tutorial already covers this in detail.

Upon opening index.html, with your browser of choice, you should see a very simple web page with just a title and some basic formatting as seen below:

figure2-basic-web-page

Loading In Our Data

The first thing we are going to need to create our TV Show app, is information about TV shows. We are going to use an API provided by Trakt.tv. Before we can get started you are going to need an API key, you can register for one on their website.

Why use this API? Do I really have to register? We are using this API so our app will use real data and will actually provide some use once completed. Also, by using this API we do not need to go over any server side implementations within this tutorial and can focus completely on AngularJS. An extra couple of minutes to register for the API will be well worth it.

Now that you have your own API key, we can utilize the Trakt API to get some information on TV shows. We are going to use one of the available API calls for this tutorial, more information on this is available in the api docs. This API call will provide us with all the TV Show Premieres within a specified time frame.

Open mainController.js and modify it to match the below code:

    app.controller("mainController", function($scope, $http){

        $scope.apiKey = "[YOUR API KEY HERE]";
        $scope.init = function() {
            //API requires a start date
            var today = new Date();
            //Create the date string and ensure leading zeros if required
            var apiDate = today.getFullYear() + ("0" + (today.getMonth() + 1)).slice(-2) + "" + ("0" + today.getDate()).slice(-2);
            $http.jsonp('http://api.trakt.tv/calendar/premieres.json/' + $scope.apiKey + '/' + apiDate + '/' + 30 + '/?callback=JSON_CALLBACK').success(function(data) {
                console.log(data);
            }).error(function(error) {

            });
        };

    });

If you look within the index.html file, for the following line:

    <div class="container main-frame" ng-app="TVPremieresApp" ng-controller="mainController" ng-init="init()">

You will see that the ng-init method is calling the init function, this means that the init() function within our mainController will be called after the page has been loaded.

If you read the API documentation for the calendar/premieres method you will have seen that it takes three parameters, your API key, the start date (e.g. 20130616) and the number of days.

To provide all three parameters, we first need to get today's date using JavaScripts Date() method and format it to the API specified date format to create the apiDate string. Now that we have everything we need, we can create an $http.jsonp call to the API method. This will allow our web app to call a URL that is not within our own domain and receive some JSON data. Ensure that ?callback=JSON_CALLBACK is prepended onto the request URI so that our attached .success callback function is called on response.

Within our .success function we then simply output the received data to the console. Open index.html within your browser and open the JavaScript console, you should see something like the following:

figure3-javascript-console

This demonstrates that we are successfully performing a call to the Trakt API, authenticating with our API key and receiving some JSON data. Now that we have our TV show data, we can move on to the step.


Displaying Our Data

Processing the JSON Objects

Before we can display our data, we need to process and store it. As the API returns the premiere episodes grouped by date, we want to remove this grouping and just create a single array with all the premiere episodes and their associated data. Modify mainController.js to be as follows:

    app.controller("mainController", function($scope, $http){
        $scope.apiKey = "[YOUR API KEY]";
        $scope.results = [];
        $scope.init = function() {
            //API requires a start date
            var today = new Date();
            //Create the date string and ensure leading zeros if required
            var apiDate = today.getFullYear() + ("0" + (today.getMonth() + 1)).slice(-2) + "" + ("0" + today.getDate()).slice(-2);
            $http.jsonp('http://api.trakt.tv/calendar/premieres.json/' + $scope.apiKey + '/' + apiDate + '/' + 30 + '/?callback=JSON_CALLBACK').success(function(data) {
                //As we are getting our data from an external source, we need to format the data so we can use it to our desired effect
                //For each day, get all the episodes
                angular.forEach(data, function(value, index){
                    //The API stores the full date separately from each episode. Save it so we can use it later
                    var date = value.date;
                    //For each episodes, add it to the results array
                    angular.forEach(value.episodes, function(tvshow, index){
                        //Create a date string from the timestamp so we can filter on it based on user text input
                        tvshow.date = date; //Attach the full date to each episode
                        $scope.results.push(tvshow);
                    });
                });
            }).error(function(error) {

            });
        };
    });

The above code is well commented and should be easy to follow, lets take a look at these changes. First, we declare a scope variable $scope.results as an array which will hold our processed results. We then use angular.forEach (which is similar to jQuery's $.each method for those who know it) to loop through each date group and store the date in a local date variable.

We then create another loop which loops through each of the TV shows within that date group, adds the locally stored date to the tvshow object and then finally adds each tvshow object to the $scope.results array. With all of this done, our $scope.results array will look like the following:

figure4-formatted-tvshow-json-objects

Creating the List HTML

We now have some data we wish to display within a list, on our page. We can create some HTML with ng-repeat to dynamically create the list elements based on the data within $scope.results. Add the following HTML code within the unordered list that has the episode-list class in index.html:

    <li ng-repeat="tvshow in results">
        <div class="row-fluid">
            <div class="span3">
                <img src="{{tvshow.episode.images.screen}}" />
                <div class="ratings"><strong>Ratings:</strong> <span class="label"><i class="icon-thumbs-up"></i> {{tvshow.episode.ratings.loved}}</span> <span class="label"><i class="icon-thumbs-down"></i> {{tvshow.episode.ratings.hated}}</span> <span class="label label-important" ng-class="{'label-success': tvshow.episode.ratings.percentage >= 50}"><strong>%</strong> {{tvshow.episode.ratings.percentage}}</div>
            </div>
            <div class="span6">
                <h3>{{tvshow.show.title}}: {{tvshow.episode.title}}</h3>
                <p>{{tvshow.episode.overview}}</p>
            </div>
            <div class="span3">
                <div class="fulldate pull-right label label-info">{{tvshow.date}}</div>
                <ul class="show-info">
                    <li><strong>On Air:</strong> {{tvshow.show.air_day}} {{tvshow.show.air_time}}</li>
                    <li><strong>Network:</strong> {{tvshow.show.network}}</li>
                    <li><strong>Season #:</strong> {{tvshow.episode.season}}</li>
                    <li><strong>Genres:</strong> <span class="label label-inverse genre" ng-repeat="genre in tvshow.show.genres">{{genre}}</span></li>
                </ul>
            </div>
        </div>
    </li>

This HTML is simply creating a single list element with ng-repeat. ng-repeat="tvshow in results" is telling angular to repeat this list element for each object within the $scope.results array. Remember that we do not need to include the $scope, as we are within an element with a specified controller (refer to the previous tutorial for more on this).

Inside the li element we can then reference tvshow as a variable which will hold all of the objects data for each of the TV shows within $scope.results. Below is an example of one of the objects within $scope.results so you can easily see how to reference each slice of data:

    {
    "show":{
        "title":"Agatha Christie's Marple",
        "year":2004,
        "url":"http://trakt.tv/show/agatha-christies-marple",
        "first_aired":1102838400,
        "country":"United Kingdom",
        "overview":"Miss Marple is an elderly spinster who lives in the village of St. Mary Mead and acts as an amateur detective. Due to her long and eventful life crimes often remind her of other incidents. Although Miss Marple looks sweet, frail, and old, she fears nothing; either dead or living.",
        "runtime":120,
        "network":"ITV",
        "air_day":"Monday",
        "air_time":"9:00pm",
        "certification":"TV-14",
        "imdb_id":"tt1734537",
        "tvdb_id":"78895",
        "tvrage_id":"2515",
        "images":{
            "poster":"http://slurm.trakt.us/images/posters/606.jpg",
            "fanart":"http://slurm.trakt.us/images/fanart/606.jpg",
            "banner":"http://slurm.trakt.us/images/banners/606.jpg"
        },
        "ratings":{
            "percentage":91,
            "votes":18,
            "loved":18,
            "hated":0
        },
        "genres":[
            "Drama",
            "Crime",
            "Adventure"
        ]
    },
    "episode":{
        "season":6,
        "number":1,
        "title":"A Caribbean Mystery",
        "overview":"\"Would you like to see a picture of a murderer?\", Jane Marple is asked by Major Palgrave whilst on a luxurious holiday in the West Indies. When she replies that she would like to hear the story, he explains. There once was a man who had a wife who tried to hang herself, but failed. Then she tried again later, and succeeded in killing herself. The man remarried to a woman who then tried to gas herself to death. She failed, but then tried again later and succeeded. Just as Major Palgrave is about to show the picture to her, he looks over her shoulder, appears startled, and changes the subject. The next morning, a servant, Victoria Johnson, finds him dead in his room. Doctor Graham concludes that the man died of heart failure; he showed all the symptoms, and had a bottle of serenite (a drug for high blood pressure) on his table.",
        "url":"http://trakt.tv/show/agatha-christies-marple/season/6/episode/1",
        "first_aired":1371366000,
        "images":{
            "screen":"http://slurm.trakt.us/images/fanart/606-940.jpg"
        },
        "ratings":{
            "percentage":0,
            "votes":0,
            "loved":0,
            "hated":0
        }
    },
    "date":"2013-06-16"
    }

As an example, within the li element, we can get the show title by referencing tvshow.show.title and wrapping it in double curly brackets:{{ }}. With this understanding, it should be easy to see what information will be displayed for each list element. Thanks to the CSS bundled with the skeleton structure, if you save these changes and open index.html within your browser, you should see a nicely formatted list of TV shows with the associated information and images. This is shown in the figure below:

figure5-formatted-show-list

Conditional Classes

You may or may not have noticed:

ng-class="{'label-success': tvshow.episode.ratings.percentage >= 50}"

...which is attached to one of the span elements, within the ratings section, in the above HTML. ng-class allows us to conditionally apply classes to HTML elements. This is particularly useful here, as we can then apply a different style to the percentage span element depending on whether the TV show rating percentage is high or not.

In the above HTML example, we want to apply the class label-success, which is a Twitter Bootstrap class, which will style the span to have a green background and white text. We only want to apply this class to the element if the rating percentage is greater than or equal to 50. We can do this as simply as tvshow.episode.ratings.percentage >= 50. Take a look down the list of formatted TV shows in your browser, if any of the percentages meet this condition, they should be displayed green.


Creating a Search Filter

We now have a list of upcoming TV show premieres, which is great, but it doesn't offer much in the way of functionality. We are now going to add a simple text search which will filter all of the objects within the results array.

Binding HTML Elements to Scope Variables

Firstly we need to declare a $scope.filterText variable within mainController.js as follows:

    app.controller("mainController", function($scope, $http){
        $scope.apiKey = "[YOUR API KEY]";
        $scope.results = [];
        $scope.filterText = null;
        $scope.init = function() {
            //API requires a start date
            var today = new Date();
            //Create the date string and ensure leading zeros if required
            var apiDate = today.getFullYear() + ("0" + (today.getMonth() + 1)).slice(-2) + "" + ("0" + today.getDate()).slice(-2);
            $http.jsonp('http://api.trakt.tv/calendar/premieres.json/' + $scope.apiKey + '/' + apiDate + '/' + 30 + '/?callback=JSON_CALLBACK').success(function(data) {
                //As we are getting our data from an external source, we need to format the data so we can use it to our desired affect
                //For each day get all the episodes
                angular.forEach(data, function(value, index){
                    //The API stores the full date separately from each episode. Save it so we can use it later
                    var date = value.date;
                    //For each episodes add it to the results array
                    angular.forEach(value.episodes, function(tvshow, index){
                        //Create a date string from the timestamp so we can filter on it based on user text input
                        tvshow.date = date; //Attach the full date to each episode
                        $scope.results.push(tvshow);
                    });
                });
            }).error(function(error) {

            });
        };
    });

Now we need to add a text input so that the user can actually input a search term. We then need to bind this input to the newly declared variable. Add the following HTML within the div which has the search-box class in index.html.

    <label>Filter: </label>
    <input type="text" ng-model="filterText"/>

Here we have used ng-model to bind this input to the $scope.filterText variable we declared within our scope. Now this variable will always equal what is inputted into this search input.

Enforcing Filtering On ng-repeat Output

Now that we have the text to filter on, we need to add the filtering functionality to ng-repeat. Thanks to the built-in filter functionality of AngularJS, we do not need to write any JavaScript to do this, just modify your ng-repeat as follows:

    <li ng-repeat="tvshow in results | filter: filterText">

It's as simple as that! We are telling AngularJS - before we output the data using ng-repeat, we need to apply the filter based on the filterText variable. Open index.html in a browser and perform a search. Assuming you searched for something that exists, you should see a selection of the results.


Creating a Genre Custom Filter

So, our users can now search for whatever they are wanting to watch, which is better than just a static list of TV shows. But we can take our filter functionality a little further and create a custom filter that will allow the user to select a specific genre. Once a specific genre has been selected, the ng-repeat should only display TV shows with the chosen genre attached.

First of all, add the following HTML under the filterText input in index.html that we added previously.

    <label>Genre: </label>
    <select ng-model="genreFilter" ng-options="label for label in availableGenres">
        <option value="">All</option>
    </select>

You can see from the above HTML that we have created a select input bound to a model variable called genreFilter. Using ng-options we are able to dynamically populate this select input using an array called availableGenres.

First of all, we need to declare these scope variables. Update your mainController.js file to be as follows:

    app.controller("mainController", function($scope, $http){
        $scope.apiKey = "[YOUR API KEY HERE]";
        $scope.results = [];
        $scope.filterText = null;
        $scope.availableGenres = [];
        $scope.genreFilter = null;
        $scope.init = function() {
            //API requires a start date
            var today = new Date();
            //Create the date string and ensure leading zeros if required
            var apiDate = today.getFullYear() + ("0" + (today.getMonth() + 1)).slice(-2) + "" + ("0" + today.getDate()).slice(-2);
            $http.jsonp('http://api.trakt.tv/calendar/premieres.json/' + $scope.apiKey + '/' + apiDate + '/' + 30 + '/?callback=JSON_CALLBACK').success(function(data) {
                //As we are getting our data from an external source, we need to format the data so we can use it to our desired affect
                //For each day get all the episodes
                angular.forEach(data, function(value, index){
                    //The API stores the full date separately from each episode. Save it so we can use it later
                    var date = value.date;
                    //For each episodes add it to the results array
                    angular.forEach(value.episodes, function(tvshow, index){
                        //Create a date string from the timestamp so we can filter on it based on user text input
                        tvshow.date = date; //Attach the full date to each episode
                        $scope.results.push(tvshow);
                        //Loop through each genre for this episode
                        angular.forEach(tvshow.show.genres, function(genre, index){
                            //Only add to the availableGenres array if it doesn't already exist
                            var exists = false;
                            angular.forEach($scope.availableGenres, function(avGenre, index){
                                if (avGenre == genre) {
                                    exists = true;
                                }
                            });
                            if (exists === false) {
                                $scope.availableGenres.push(genre);
                            }
                        });
                    });
                });
            }).error(function(error) {

            });
        };
    });

It is obvious that we have now declared both genreFilter and availableGenres which we saw referenced within our HTML. We have also added some JavaScript which will populate our availableGenres array. Within the init() function, while we are processing the JSON data returned from the API, we are now doing some additional processing and adding any genres that are not already within the availableGenres array to this array. This will then populate the select input with any available genres.

If you open index.html within your browser, you should see the genre select drop down populate as illustrated below:

figure6-genre-select-drop-down

When the user chooses a genre, the $scope.genreFilter variable will be updated to equal the selected value.

Creating the Custom Filter

As we are wanting to filter on a specific part of the TV show objects, we are going to create a custom filter function and apply it alongside the AngularJS filter within the ng-repeat.

At the very bottom of mainController.js, after all of the other code, add the following JavaScript:

    app.filter('isGenre', function() {
        return function(input, genre) {
            if (typeof genre == 'undefined' || genre == null) {
                return input;
            } else {
                var out = [];
                for (var a = 0; a < input.length; a++){
                    for (var b = 0; b < input[a].show.genres.length; b++){
                        if(input[a].show.genres[b] == genre) {
                            out.push(input[a]);
                        }
                    }
                }
                return out;
            }
        };
    });

The above JavaScript declares a custom filter to our app called isGenre. The function within the filter takes two parameters, input and genre. input is provided by default (which we will see in a moment) and is all the data that the ng-repeat is processing. genre is a value we need to pass in. All this filter does, is take the specified genre and checks to see if each of the TV show objects within input have the specified genre attached to them. If an object has the specified genre, it adds it to the out array, which will then be returned to the ng-repeat. If this doesn't quite make sense, don't worry! It should shortly.

Applying the Custom Filter

Now that we have our customer filter available, we can add this additional filter to our ng-repeat. Modify your ng-repeat in index.html as follows:

    <li ng-repeat="tvshow in results | filter: filterText | isGenre:genreFilter">

This simply chains another filter onto the ng-repeat output. Now the output will be processed by both filters before it is displayed on the screen. As you can see we have specified our custom filter as isGenre: and then we are passing the scope variable genreFilter as a parameter, which is how we provide our customer filter with the genre variable we talked about earlier. Remember that AngularJS is also providing our filter with the data that the ng-repeat is processing as the input variable.

OK, our custom genre filter is complete. Open index.html in a browser and test out the new functionality. With this filter in place, a user can easily filter out genres they are not interested in.


Calling Scope Functions

You may have noticed that each TV show listing also shows the genre itself. For some additional functionality, we are going to allow the user to click these genres, which will then automatically apply the genre filter for the genre they have clicked on. First of all, we need to create a scope function that the ng-click can call. Add the following code within the mainController on mainController.js:

    $scope.setGenreFilter = function(genre) {
        $scope.genreFilter = genre;
    }

In the above code, this function takes a genre value and then sets the $scope.genreFilter to the specified value. When this happens, the genre filter select box's value will update and the filter will be applied to the ng-repeat output. To trigger this function when the genre span elements are clicked, add an ng-click to the genre span elements within index.html as follows:

    <span class="label label-inverse genre" ng-repeat="genre in tvshow.show.genres" ng-click="setGenreFilter(genre)">{{genre}}</span>

The ng-click calls our previously created setGenreFilter function and specifies a genre. Open index.html and try it out!


Custom Ordering With AngularJS

Our TV show premiere app is looking pretty good, users can easily refine the results displayed using a series of intuitive filters. To enhance this experience we are going to add some custom ordering functionality so our users will be able to choose a range of ordering options.

Add the following HTML under the genre select drop down:

    <label>Order by: </label>
    <select ng-model="orderField" ng-options="label for label in orderFields" class="input-medium"></select>
    <select ng-model="orderReverse"class="input-medium">
        <option value="true">Descending</option>
        <option value="false">Ascending</option>
    </select>

With this code added, we have two more drop downs. One to select how to order the data and another to choose the direction in which to order the data. We now need to create a function within our controller to make the order comparison. Add the following JavaScript under our setGenreFilter function:

    $scope.customOrder = function(tvshow) {
        switch ($scope.orderField) {
            case "Air Date":
                return tvshow.episode.first_aired;
                break;
            case "Rating":
                return tvshow.episode.ratings.percentage;
                break;
        }
    };

We also need to declare some additional scope variables:

    $scope.orderFields = ["Air Date", "Rating"];
    $scope.orderDirections = ["Descending", "Ascending"];
    $scope.orderField = "Air Date"; //Default order field
    $scope.orderReverse = false;

If you now open index.html within your browser, you should see the added drop downs populated with Air Date already selected as the default order field. This is shown in the figure below:

figure7-order-drop-downs

Finally, as we have done with our other filters, we are going to need to append this to our ng-repeat, update this as follows:

    <li ng-repeat="tvshow in results | filter: filterText | isGenre:genreFilter | orderBy:customOrder:orderReverse">

We are now applying an order-by-filter on our data in addition to the other filters. We are telling the order by to use our customOrder function and we are passing our orderReverse scope variable through as well. Open index.html in a browser and see the ordering in action.


Conclusion

AngularJS has allowed us to quickly create a detailed and functional web application with minimum effort. Utilizing AngularJS's built-in filter functions, alongside some of our own custom code, our web application allows our users to easily filter and search through the TV show premieres.

After reading this tutorial you should now be able to understand and use the following principles:

  • Using ng-repeat to display information on screen.
  • Binding to inputs, allowing users to search and filter ng-repeat output.
  • Chaining filters on ng-repeat to perform multiple filtering functions.
  • Custom ordering of data.
  • Using events such as ng-click to respond to user interaction.
  • Using ng-class to conditionally apply styling to page elements.

So in conclusion, the topics covered in this tutorial should give you a strong foundation and understanding of what you can achieve when creating rich web applications in AngularJS.

Related Posts
  • Web Design
    HTML & CSS
    Build a Dynamic Grid with Salvattore and Bootstrap in 10 MinutesSalvatorre thumb
    Today, we will use Salvattore in combination with Twitter Bootstrap 3 to make a responsively awesome flowing grid structure.Read More…
  • Code
    Theme Development
    Custom Controls in the Theme CustomizerTheme customizer custom control 400
    In the last article, we explored the advanced controls available in the Theme Customizer, and how to implement them. We’re going to look at how to create our own custom control, allowing you to choose which Category of Posts are displayed on the home page. To get started, download version 0.6.0 of our Theme Customizer Example.Read More…
  • Web Design
    UX
    Walk Users Through Your Website With Bootstrap TourTour retina
    When you have a web application which requires some getting used to from your users, a walkthrough of the interface is in order. Creating a walkthrough directly on top of the interface makes things very clear, so that's what we're going to build, using Bootstrap Tour.Read More…
  • Code
    JavaScript & AJAX
    Ember Components: A Deep DiveEmber components retina preview
    Ember.js is a JavaScript MVC framework that allows developers to create ambitious web applications. Although pure MVC allows a developer to separate concerns, it does not provide you with all the tools and your application will need other constructs. Today, I'm going to talk about one of those constructs. Ember components are essentially sandboxed re-usable chunks of UI. If you are not familiar with Ember, please check out Getting Started With Ember.js or the Let's Learn Ember Course. In this tutorial, we will cover the Web Components specification, learn how to write a component in Ember, talk about composition, explain the difference between an Ember view and an Ember component, and practice integrating plugins with Ember components.Read More…
  • Code
    JavaScript & AJAX
    Working With IndexedDB - Part 3Indexeddb retina preview
    Welcome to the final part of my IndexedDB series. When I began this series my intent was to explain a technology that is not always the most... friendly one to work with. In fact, when I first tried working with IndexedDB, last year, my initial reaction was somewhat negative ("Somewhat negative" much like the Universe is "somewhat old."). It's been a long journey, but I finally feel somewhat comfortable working with IndexedDB and I respect what it allows. It is still a technology that can't be used everywhere (it sadly missed being added to iOS7), but I truly believe it is a technology folks can learn and make use of today. In this final article, we're going to demonstrate some additional concepts that build upon the "full" demo we built in the last article. To be clear, you must be caught up on the series or this entry will be difficult to follow, so you may also want to check out part one.Read More…
  • Code
    JavaScript & AJAX
    Working With IndexedDB - Part 2Indexeddb retina preview
    Welcome to the second part of my IndexedDB article. I strongly recommend reading the first article in this series, as I'll be assuming you are familiar with all the concepts covered so far. In this article, we're going to wrap up the CRUD aspects we didn't finish before (specifically updating and deleting content), and then demonstrate a real world application that we will use to demonstrate other concepts in the final article.Read More…