Tag Archives: frontend

Filtering and ordering lists with AngularJS

angularJS logo

Recently, I have been spending quite a bit of time playing around with AngularJS. I mostly like it although occasionally “the magic” doesn’t happen and then it is very hard to figure out what went wrong because everything is just so “magical”. By that I mostly mean that bindings between controllers and views are so very automatic that it’s hard to see where they fail, when they fail. Luckily, that doesn’t happen so often and the basic techniques are really easy to pick up.

However, what I especially like about AngularJS is how easy it is to achieve pretty smooth interface interactions. What follows is a small description of  using AngularJS to show a number of records and filter them based on some of their values.

The (much simplified) app I am building here will display a list of books in an imaginary collection, including their title, author, publish year, genre and their rating. The hypothetical visitors to the app will also be able to vote on books to create a top 10 list. They will also be able to order the list by author, title, year, or number of points. And finally, it will be possible to display books from only one genre. You can see a demo of this app here.

Here is what I am using in this project:

  1. AngularJS script and web-server.js that you can find in the seed project (not necessary but it will make your life easier). You can read here how to install node.js and run the server script.
  2. Bootstrap (just for the css)

My apps structure looks like this:

BookCollection file structure

For this tutorial fonts and img folders are irrelevant. The scripts folder holds angular.js and web-server.js file.

In a real application the data for the website would come from a database, but for simplicity’s sake, I will just write an array of objects and get data from there.

Step 1: AngularJS Module

The first thing to do when writing an AngularJS app is to create the module. This is done in the js/app.js file:

var BookCollectionApp = angular.module('BookCollectionApp', []);

All this code does is creating a variable for your angular module. You will use this variable to refer to this module. In a more complex application you might use this file to inject dependencies to your module but in this case this is not necessary.

Step 2: Data

The second piece of code, you want to write is the data factory (js/services/BooksData.js):

BookCollectionApp.factory('BooksData', function(){
	return [
		{title: "To Kill the Mocking Bird", author: "Harper Lee", year: "1960", genre: "Southern Gothic", points: 0},
		{title: "Feet of Clay", author: "Terry Pratchett", year: "1996", genre: "Fantasy", points: 0},
		{title: "Solaris", author: "Stanisław Lem", year: "1961", genre: "SciFi", points: 0},
		{title: "Neverwhere", author: "Neil Gaiman", year: "1996", genre: "Fantasy", points: 0},
		{title: "The Wind-up Bird Chronicle", author: "Haruki Murakami", year: "1997", genre: "Who the hell knows?!", points: 0}
	]
});

In this file I register a factory service called BookData on my app (BookCollectionApp). It will return an array of objects. Each object is a single book bascwith title, author, year, genre and a default point value.

Step 3: AngularJS Controller

Now, it is time to create a controller in the js/controllers/BookCollectionController.js that will be registered on the BookCollectionApp module and that will expose the factory data to potential views:

BookCollectionApp.controller('BookCollectionController', function($scope, BooksData){
	$scope.Books = BooksData;
});

This is a very basic controller file. We will expand it a little bit later but for now this is all that is needed. I register my controller on the BookCollectionApp and I give the controller “BookCollectionController” name. In the controller function I pass scope and BooksData factory and create “Books” variable in my scope giving it the value of what the BooksData factory returns (the array of book objects);

Step 4: View file

With this basic setup, I can move on to building a view that will display books as a list. A View is basically a regular HTML template with data injected through AngularJS. In this case we are talking about index.html file:

<!doctype html>

<meta charset="UTF-8" />
Document
		<link href="css/bootstrap.min.css" rel="stylesheet" />
		<link href="css/style.css" rel="stylesheet" />

On the html tag, I use the ng-app directive to tell my view which module it belongs to.

<body class="container">
	<h1>Book collection</h1>
	<div ng-controller="BookCollectionController" class="row col-md-12">
		<ul class="row col-md-12 books">
			<li ng-repeat="book in Books" class="row col-md-12">
				<div>
					<h3 class="title">{{book.title}}</h3>
					<span class="author">{{book.author}}</span>
					<span class="year">{{book.year}}</span>
					<span class="genre">{{book.genre}}</span>
				</div>
			</li>
		</ul>
	</div>
	<script src="scripts/angular.js"></script>
	<script src="js/app.js"></script>
	<script src="js/services/BooksData.js"></script>
	<script src="js/controllers/BookCollectionController.js"></script>
	<script src="js/bootstrap.min.js"></script>
</body>
</html>

The important bits here are these 3 directives the:

  1. ng-controller – tells the controller on which html elements it should operate.
  2. ng-repeat – repeats the content of the tag for every item in the selection. In this case, it tells the li and everything inside to repeat for every book in the Books variable we created and exposed in the controller
  3. curly braces notation – tells AngularJS to replace the object property with the value of that property. So the book.title will be replaced with the title of each book that the code iterates through

All the necessary scripts are referenced at the end of the file. The result of this code is a simple rendering of the book collection. Now we can move on to more fun properties of AngularJS.

Step 5: Voting functionality

I want to allow visitors to the website to vote on the books, so that I can make a top 10 list. In order to do that, I need to amend 2 files. First, I have to edit the view file so that it has the interface  to display points and vote. I also need to bind it the interface elements to the controller. Then I need to adjust the controller itself so it can handle the voting.

In the index.html, I will adjust the li element like so:

<li ng-repeat="book in Books" class="row col-md-12">
	<div>
		<h3 class="title">{{book.title}}</h3>
		<span class="author">{{book.author}}</span>
		<span class="year">{{book.year}}</span>
		<span class="genre">{{book.genre}}</span>
		<div class="row col-md-12 votes">
			<a class="voteUp" ng-click="voteUp(book)">+</a>
			<span class="points">{{book.points}}</span>
			<a class="voteDown" ng-click="voteDown(book)">-</a>
		</div>
	</div>
</li>

The extra div contains two links and a span. The span simply displays current amount of points. The links have ng-click directive which listens to the click directive on these elements and calls voteUp or voteDown functions. The functions (that we will write in the controller) have the book object passed to them. That means that for every li element, the voteUp function will have access to the correct book object and will be able to change the amount of votes.

And here comes the controller that takes care of the functions.

BookCollectionApp.controller('BookCollectionController', function($scope, BooksData){
	$scope.Books = BooksData;

	$scope.voteUp = function (book) {
		book.points++;
	}

	$scope.voteDown = function (book) {
		book.points--;
	}
});

In this controller, I create voteUp and voteDown functions on the scope (so that the view actually has access to them). The voteUp function adds one to book.points property value and voteDown, surprisingly, detracts one point from the book.points value.

Thanks to two-way binding, every time a user clicks on a plus or minus sign, the object will be edited and the new value will be displayed. Of course this application is not actually using any storage, so the edits will be lost between page reloads.

Step 6: Filtering with AngularJS

And now the moment we’ve all been waiting for. The super easy filtering in AngularJS. So in this app I want to be able to order the list according to some properties on the objects. I also want to display objects of only one type. To do that, I need to start by creating the interface to choose the filter/order by principle:

Order by:
<select ng-model="sortorder">
	<option value="default" selected>None</option>
	<option value="title">Title</option>
	<option value="author">Author</option>
	<option value="-points">Points</option>
</select>
Show:
<select ng-model="showGenre">
	<option value=" " selected>All</option>
	<option ng-repeat="genre in genres" value="{{genre}}">{{genre}}</option>
</select>

This code is added just under the div ng-controller directive. It creates two dropdown lists but I could just as well have lists with links instead or radiobuttons etc. What actually matters is the ng-model directive (you can call it what you like). I’ll use it in the list of books to inform it where it should look for the filter criteria.

Notice that value for points has a “-” sign in front of it. This is to ensure that items are ordered by points from the highest to lowest value and not the opposite (as is the default).

Because, I don’t know in advance what genres I might have in my collection, I create the list dynamically based on the books in the collection. The easiest way would be to simply type:

<option ng-repeat="book in Books" value="{{book.genre}}">{{book.genre}}</option>

This of course would mean that if I had two books of the same genre, the genre will be shown twice in the dropdown. So instead in my controller I will create a new array by pushing none-repeating genres. Here is how the piece of code looks:

$scope.genres = []
	angular.forEach($scope.Books, function(value, genre){
		if($scope.genres.indexOf(value.genre) == -1)
		{
			$scope.genres.push(value.genre);
		}
	});

Now in my view, I can iterate through genres knowing that in the controllers I took care to only store the relevant items.

When the interface is in place, I can take care of actually reacting to the filter parameters that the user has chosen. And here is where the real genius of AngularJS shows. All I need to do is amend this line of code in my view:

<li ng-repeat="book in Books | orderBy:sortorder | filter:showGenre" class="row col-md-12">

Now I told AngularJS to pay attention to sortorder and showGenre models and react to changes on them by applying respective sort orders and filters.

Et voila! Application is ready.