News

AngularJS: How to build a listing app

Discover how to use Angular $http requests and the resulting JSON data to create a filtered listing

Angular

Angular

The days of users waiting for page reloads when filtering and ordering information are long gone. With the increased capability of browsers to run more complex code, JS applications are becoming a key part of most website builds. AngularJS is a powerful application framework that makes large applications easier to create.

The data and presentation layers are clearly separated and easy to manage. With its own HTTP request methods and a cut-down version of jQuery built in (jqLite), AngularJS  provides all the tools needed for web app development.
We will cover the app setup, retrieval of data from web services, binding data to your HTML and then filtering it for the user. All Controllers, Services and Filters are created within Angular modules, making each module reusable and the app easy for other developers to extend. It is easier to manage these modules as separate files but it is recommended that these files are bundled together in a live environment, reducing HTTP requests for the app.

DOWNLOAD TUTORIAL FILES

Set up the HTML

Start by adding a reference to AngularJS from the Google CDN to your HTML page. Then, add a reference to your application on the body tag using an attribute as shown. If you use the ‘data-ng-’ prefix for your Angular attributes as opposed to ‘ng-’, your HTML will be valid.

001    <!DOCTYPE html>
002    <html lang=”en” xmlns=”http://www.w3.org/1999/xhtml”>
003    <head>
004        <meta charset=”utf-8” />
005        <title>Angular Listing App</title>
006        <script src=”http://ajax.googleapis.com/ajax/libs/
angularjs/1.2.15/angular.min.js”></script>
007    </head>
008    <body data-ng-app=”listingApp”>
009    </body>
010    </html>

Create the scripts

In your scripts folder, create a folder for modules. Then create a file named ‘myApp.js’ in the scripts folder and a file named ‘listing.js’ in the modules folder – be sure to reference these in your HTML page. Add the following code to these files; this defines your core application and uses dependency injection to add your module to this application.

001    //modules/listing.js
002    angular.module(‘listing.module’,[])
003    //myApp.js
004    var myApp = angular.module
(‘listingApp’, [‘listing.module’]);

Add a controller

Define a controller in listing.js and pass in $scope – all data and methods defined in this controller will extend $scope. Add a reference to your new controller in your HTML page and you can then display any data defined in $scope. In a browser window you should now see the $scope.title value displayed in a h1 tag.

001    <!DOCTYPE html>
002    <html lang=”en” xmlns=”http://www.w3.org/1999/xhtml”>
003    <head>
004        <meta charset=”utf-8” />
005        <title>Angular Listing App</title>
006        <script src=”http://ajax.googleapis.com/ajax/libs/
angularjs/1.2.15/angular.min.js”></script>
007        <script src=”scripts/modules/listing.js”></script>
008        <script src=”scripts/myApp.js”></script>
009    </head>
010    <body data-ng-app=”listingApp”>
011        <section data-ng-controller=”listingCtrl”>
012            <h1>{{title}}</h1>
013        </section>
014    </body>
015    </html>
016    //modules/listing.js
017    angular.module(‘listing.module’, [])
018    .controller(‘listingCtrl’, [‘$scope’, function 
($scope) {
019        “use strict”;
020        $scope.title = “Technology News”;
021    }]);
022    

Create a data service

Create a services folder and a file named ‘listingServices.js’. In this file you can create a services module with a data service providing GET and POST methods using Angular’s $http methods. These GET and POST methods accept a callback function to pass the retrieved data to.

001    angular.module(‘listing.services’, [])
002    .service(‘data’, [‘$http’, function ($http) {
003        “use strict”
004        this.get = function (url, callback) {
005            $http({ method: ‘GET’, url: url }).
006            success(function (data, status, headers, config) {
007                // this callback will be called asynchronously
008                // when the response is available
009                callback(data);
000            }).
011            error(function (data, status, headers, config) {
012                // called asynchronously if an error occurs
013                // or server returns response with an error 
status.
014                throw “No data returned from “ + url;
015            });
016        };
017        this.post = function (url, callback, obj) {
018            $http({ method: ‘POST’, url: url, data: obj }).
019            success(function (data, status, headers, config) {
020                // this callback will be called asynchronously
021                // when the response is available
022                callback(data);
023            }).
024            error(function (data, status, headers, config) {
025                // called asynchronously if an error occurs
026                // or server returns response with an error 
status.
027                throw “No data returned from “ + url;
028            });
029        };
030    }]);

Create some test data

Now we have a service to make HTTP requests, we need some data. If you are working with a web service that is already available to you then you can use this. In this tutorial we will be using some test JSON data as a separate file, created using www.jsoneditoronline.org with the following structure.

001    {
002        “articles”: [
003            {
004                “title”: “Science behind the D-Day landings”,
005                “description”: “How innovations in science and 
engineering made the D-Day landings possible 
with giant troop tanks and tanks that could 
drive on water.”,
006                “pubDate”: “2014-02-14”,
007                “tags”: [
008                    “tag1”,
009                    “tag4”
010                ]
011            },
012            {
013                “title”: “...”,
014                “description”: “...”,
015                “pubDate”: “...”
016                “tags”: [
017                    ...
018                ]      
019            }
020        ]
021    }

Add your service

To use the methods within your service you need to add the reference to your service script in your HTML page and inject this module and its methods into your controller as shown. You can pass a URL and callback function to the ‘data.get’ method. You should now see the data logged out in your browser console.

001    angular.module(‘listing.module’, [‘listing.services’])
002    .controller(‘listingCtrl’, [‘$scope’,’data’, function 
($scope, data) {
003        ‘use strict’;
004        $scope.title = “Technology News”;
005        $scope.setData = function (data) {
006                $scope.articles = data.articles;
007                console.log($scope.articles);
008        }
009        data.get(‘articles.json’, $scope.setData)
010    }]);

Render your data

Now the data is stored within the controller $scope it can be displayed within the HTML page. For an array of items a repeater can be used that will render the HTML element bound for each item in the data set. Properties held within each data item are also available to be rendered as shown with the ‘title’ and ‘description’ properties.

001    <ul>
002        <li data-ng-repeat=”article in articles”>
003            <h2>{{article.title}}</h2>
004            <p>{{article.description}}</p>
005        </li>
006    </ul>

Handling dates

Dates can be stored in a range of formats and can be converted within an Angular binding. Simply add ‘| date’ after your property reference and your chosen format option eg article.pubDate | date:’m/d/yy’. You can see the range of format options available at docs.angularjs.org/api/ng/filter/date.

001    <ul>
002        <li data-ng-repeat=”article in articles”>
003            <h2>{{article.title}}</h2>
004            <p>{{article.pubDate | date:’dd/MM/yy’}}</p>
005            <p>{{article.description}}</p>
006        </li>
007    </ul>
008    //modules/listing.js
009    $scope.viewLimit = 4;

Ordering and Limiting

To order the listing, an ‘orderBy’ filter can be added to the repeater; this can be followed by ‘:true’ to reverse this ordering. To control the number of items shown in the list a ‘limitTo’ filter can be set. The value for limitTo must be a number and we can store this in our controller as $scope.viewLimit.

001    <ul>
002        <li data-ng-repeat=”article in articles | 
orderBy:’pubDate’:true | limitTo: viewLimit”>
003            <h2>{{article.title}}</h2>
004            <p>{{article.pubDate | date:’dd/MM/yy’}}</p>
005            <p>{{article.description}}</p>
006        </li>
007    </ul>

View more

Now add a button after your listing labelled ‘View more’. Use the Angular click binding to increase the $scope.viewLimit value and display more articles. In this example the number will be incremented by eight each time. The Angular hide binding is also used to hide the button; if all data items are displayed, it expects an expression returning a boolean value.

001    <button data-ng-click=”viewMore(8)” data-ng-
hide=”viewLimit >= articles.length”>view more</button>
002    //modules/listing.js
003    $scope.viewMore = function(num){
004        $scope.viewLimit += num;
005    }

Add a sort option

As the listing is already ordered, it is simple to provide a select drop-down for the user to adjust the ordering. The Angular model binding can be used to bind any HTML element to an item in the model or $scope. Create $scope.descending in your listing module and then build a select drop-down bound to it. Now replace the ‘true’ value for reversing the orderBy filter with a reference to this new property.

001    <div class=”controls”>
002        <label for=”sortBy”>Sort by</label>
003        <select id=”sortBy” data-ng-model=”descending”>
004            <option value=”true”>Newest first</option>
005            <option value=”false”>Oldest first</option>
006        </select>
007    </div>
008    <ul>
009        <li data-ng-repeat=”article in articles | 
orderBy:’pubDate’:descending | limitTo: viewLimit”>
010            <h2>{{article.title}}</h2>
011            <p>{{article.pubDate | date:’dd/MM/yy’}}</p>
012            <p>{{article.description}}</p>
013        </li>
014    </ul>
015    //modules/listing.js
016    $scope.descending = true;
017    

Simple text filter

Add a text input to your page with an Angular model binding. Then, add this new model property as a filter on your repeater. As there is no need to set a default, this model binding doesn’t have to be explicitly created in the $scope. This will update the listed items as the text field is typed, matching by string.

001    <div class=”controls”>
002        <input type=”text” data-ng-model=”query” placeholder
=”search news” />
003        <label for=”sortBy”>Sort by</label>
004        <select id=”sortBy” data-ng-model=”descending”>
005            <option value=”true”>Newest first</option>
006            <option value=”false”>Oldest first</option>
007        </select>
008    </div>
009    <ul>
010            <li data-ng-repeat=”article in articles | 
filter: query | orderBy:’pubDate’:descending | 
limitTo: viewLimit”>
011            <h2>{{article.title}}</h2>
012            <p>{{article.pubDate | date:’dd/MM/yy’}}</p>
013            <p>{{article.description}}</p>
014        </li>
015    </ul>

Custom filtering

A custom filter can be created as a module and injected into your listing module. This receives the array of items and a filter object and returns a new filtered array. Create a new folder named ‘filters’ and a new file named listingFilters.js with an Angular module as shown.

001    angular.module(‘listing.filters’, [])
002    .filter(‘newsFilter’, function () {
003        return function (items, filters) {
004        }
005    });
006    //modules/listing.js
007    angular.module(‘listing.module’, [‘listing.services’, 
‘listing.filters’])...

Use angular.forEach

Now the angular.forEach method can be used to iterate over the data items to be filtered. Add this method, passing in the items and change the ‘filter’ on your listing repeater to ‘newsFilter’. If you log out each item you can check this is running correctly before building your filters object.

001    <ul>
002        <li data-ng-repeat=”article in articles 
| newsFilter: filters | orderBy:’pubDate’:descending
| limitTo: viewLimit”>
003            <h2>{{article.title}}</h2>
004            <p>{{article.pubDate | date:’dd/MM/yy’}}</p>
005            <p>{{article.description}}</p>
006        </li>
007    </ul>
008    //filters/listingFilters.js
009    angular.module(‘listing.filters’, [])
010    .filter(‘newsFilter’, function () {
011        return function (items, filters) {
012            angular.forEach(items, function (item) {
013                    console.log(item);
014            });
015            return items;
016        }
017    });
018    

Create some filters

In your controller $scope create a filters object to include your search text property (remember to change the reference to ‘query’ in your HTML page) and an array of tags. Each tag should have label[string] and selected[boolean] properties. You can then render a checkbox for each tag in your HTML page, as shown below.

001    <input type=”text” data-ng-model=”filters.
query” placeholder=”search news” />
002    <div class=”clear”>
003        <div data-ng-repeat=”tag in filters.
tags” class=”checkbox”>
004            <label for=”{{tag.label}}”>{{tag.label}}</label>
005            <input id=”{{tag.label}}” type=”checkbox” data-ng
-checked=”tag.selected” />
006        </div>
007    </div>
008    //modules/listing.js
009    $scope.filters = {
010        query: “”,
011        tags: [
012            {
013                label: “tag1”,
014                selected: false
015            },
016            {
017                label: “tag2”,
018                selected: false
019            },
020            {
021                label: “tag3”,
022                selected: false
021            },
023            {
024                label: “tag4”,
025                selected: false
026            }
027        ]
028    }

Filter for text

In the filter module now add a check against filter.query. Start with an empty array named ‘filtered’, then check each item for the search text defined. If the item matches, add it to the filtered array., then return the filtered array.

Filter for tags

Extend the filter function to check through all selected tags and compare against the tags of each item. This filter can be extended to support any data structure or range of filters you need. Just make sure that the filters are all held within the filters object passed to this function and you cater for any empty or null values.

Fix the view more button

Once the filtering is implemented you may notice that the ‘View more’ button does not get hidden when fewer items match the filters. This is because it is bound to the total number of items. Instead, you can access the total number of filtered items by creating a new ‘filtered’ variable within your HTML.

001    <ul>
002        <li data-ng-repeat=”article in filtered = 
(articles | newsFilter: filters) | orderBy:
’pubDate’:descending | limitTo: viewLimit”>
003            <h2>{{article.title}}</h2>
004            <p>{{article.pubDate | date:’dd/MM/yy’}}</p>
005            <p>{{article.description}}</p>
006        </li>
007    </ul>
008    <button class=”btn” data-ng-click=”viewMore(8)” 
data-ng-hide=”viewLimit >= filtered.length”>view 
more</button>

Results numbering

Now the total number of filtered items and total number of items can be displayed to the user by simply adding a text element above the listing. This is a common requirement for filtered listings.

001    <p>{{filtered.length}} articles of {{articles.length}}
match your search.</p>

Use multiple controllers

In many applications you will need to share Properties in the $scope of one controller with another. As $scope relates to a specific controller you need to create $rootScope to share properties and inject it into each controller. Create a run function on your controller and add a test property to $rootScope.

001    .run([‘$rootScope’, function ($rootScope) {
002        $rootScope.testValue = “I am in rootScope”;
003    }]);

Share $rootScope

Now you can share $rootScope by injecting it into each controller you need to. As well as providing access to anything defined within $rootScope, it can also be extended from any controller. Make a new controller in your listing module and add this new controller into your HTML page.

001    <section data-ng-controller=”testCtrl”>
002        <h2>{{dataFromRoot}}</h2>
003    </section>
004    //modules/listing.js
005    .controller(‘testCtrl’, [‘$scope’, ‘$rootScope’, 
function ($scope, $rootScope) {
006        $scope.dataFromRoot = $rootScope.testValue;
007    }])
008    .run([‘$rootScope’, function ($rootScope) {
009        $rootScope.testValue = “I am in rootScope”;
010    }]);
011    

Utilise $watch

If you have a property or dataset being changed within $rootScope you can use the $watch method to listen for that change and then act upon it. This provides a vital connection across multiple controllers and modules, allowing you to build much more complex applications.

001    $rootScope.$watch(‘testData’, function(newValue, 
oldValue){
002        // $rootScope.testData has been updated
003    });
004
×