Cacomania: A simple RESTful AngularJS bookmark app

Cacomania

A simple RESTful AngularJS bookmark app

Guido Krömer - 6. May 2013 - Tags: , ,

Welcome to the second part of my AngularJS tutorial, you can read the first part here which covers a RESTful API build with the Slim PHP micro framework. AngularJS is a JavaScript framework for building single page applications in a MVC style. Unlike the traditionally way with jQuery you do not have to add event listeners on CSS classes and select HTML elements by querying them.

The AngularJS way is straightforward, but slightly different, you have to create an initial layout file, which is the index.html for example. This file contains which AngularJS modules has to be used and in which part of the page a view gets loaded. Although needed is a module containing the route configuration and at least one controller. A route defines which view should get displayed and which controller should handle the view. The last step is creating the view as a normal HTML file. After those three steps the single page app is working.

Styling

Giving the app a decent style if a tough work if you are not a designer. Since I'm not a designer I just took the Cosmo style from Bootswatch that is based on Bootstrap. The result is not individual, but good looking page without wasting time styling the app.

The index file

An AngularJS application start with a normal HTML file containing the base layout, all CSS and JS files which has to be loaded and some AngularJS attributes. The ng-app directive, defined as attribute in the html tag, bootstraps the whole application. The attributes value is optional and defines which module should be used. Another important directive is the ng-view, which tells AngulaJS where the module can load the view when a matching route was found. Something to note is the navigation bars add button, it is a normal HTML link, but it contains the route #/add which loads the add view in the view area defined by the ng-view directive when the link is clicked. How routing works in detail gets explained later.

<!DOCTYPE html>
<html lang="en" ng-app="cacoBookMark">
<head>
    <meta charset="utf-8">
    <title>caco[miniBookMark]</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="A simple bookmark app.">
    <meta name="author" content="Guido Krömer">
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <link href="css/app.css" rel="stylesheet">
</head>

<body>
<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="navbar-inner">
        <div class="container">
            <a class="brand" href="#/">caco[miniBookMark]</a>
            <a href="#/add/" class="btn btn-success pull-right"><i class="icon-plus icon-white"></i> Add</a>
        </div>
    </div>
</div>
<div class="container">
    <div class="row">
        <div class="span12" ng-view ng-animate="{enter: 'fade-enter'}">
        </div>
    </div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.4/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.4/angular-resource.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

The route configuration

Before I'm going to describe how a view in agular works, I think this little look ahead at the route configuration helps to understand how and when a view gets loaded. There is one route for each view/template, since we have three templates there are three routes, the initial route '/' loads the list.html template and binds the BookMarkCtrl controller on it. We have just one controller the BookMarkCtrl which gets bind on each route. The other routes are the '/add' route which gets called by clicking on the add button and the '/edit/:id' route which displays the given bookmark in a form for editing it.

The $routeProvider gets injected in our module configuration. Therefore the routes get configured only when the module gets loaded by defining it with the ng-app directive.

angular.module('cacoBookMark').config(['$routeProvider', function ($routeProvider) {
    $routeProvider.when('/add',      {templateUrl: 'partials/add.html',  controller: BookMarkCtrl})
                  .when('/edit/:id', {templateUrl: 'partials/edit.html', controller: BookMarkCtrl})
                  .when('/',         {templateUrl: 'partials/list.html', controller: BookMarkCtrl});
}]);

Views

The templating system of angular is really awesome, it does not mix HTML and the programming language together which ends having two different languages mixed in one template file. HTML elements get conditionally added if a directive's value, which is a normal attribute, is true for example.

A simple template displaying "please login" if the user is not logged in would look in the Razor ASP.NET view engine, which is a really nice and clean view engine, likes this:

@if (user.loggedin) {
<p>please login</p>
}

The AngularJS equivalent would look like this:

<p ng-show="user.loggedin">please login</p>

Quite awesome isn't it?

add.html

This view is very simple, it just displays a simple form for adding a new bookmark. The input elements use the ng-model directive, the value of this attribute is the name of the model. The ng-submit directive binds the submit event on the given method of the controller, which is the add() action in this form.

<form class="form-horizontal" ng-submit="add()">
    <fieldset>
        <legend>Add a bookmark</legend>
        <div class="control-group">
            <label class="control-label" for="add-title">Title</label>

            <div class="controls">
                <input id="add-title" name="title" type="text" placeholder="A title"
                       ng-model="newBookMark.title"/>
            </div>
        </div>
        <div class="control-group ">
            <label class="control-label" for="add-url">URL</label>

            <div class="controls">
                <input id="add-url" name="url" type="url" placeholder="www.cacodaemon.de" required
                       ng-model="newBookMark.url"/>
            </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-success"><i class="icon-plus icon-white"></i> Add</button>
        </div>
    </fieldset>
</form>

The add() action which has been bind on the submit event with the ng-submit directive is listed below. The Rest object has an add() method, which second parameter is the object which should send as POST, this is the newBookMark model. If you have read the first part of this tutorial you might have noticed that the RESTful API call for adding a bookmark has the two POST params, URL and title. The form has two corresponding fields with the ng-model newBookMark.title and newBookMark.url. Since the back end naming has been used in the front end no conversion is needed and the add method is as tiny as it is.

$scope.add = function () {
    Rest.add({}, $scope.newBookMark, function (data) {
        $location.path('/');
    });
};

AngularJS Bookmark App, add view.
This is how the add view looks like.

edit.html

The edit form is very similar to the add form with just two differences, the controller action bind on the ng-submit attribute and an additional hidden field storing the id of the bookmark going to be edited.

<form class="form-horizontal" ng-submit="save()">
    <input type="hidden" ng-model="bookmark.id"/>
    <fieldset>
        <legend>Edit a bookmark</legend>
        <div class="control-group">
            <label class="control-label" for="edit-title">Title</label>

            <div class="controls">
                <input name="title" id="edit-title" type="text" placeholder="A title" ng-model="bookmark.title"/>
            </div>
        </div>
        <div class="control-group">
            <label class="control-label" for="edit-url">URL</label>

            <div class="controls">
                <input name="url" id="edit-url" type="url" placeholder="www.cacodaemon.de" ng-model="bookmark.url"
                       required/>
            </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-warning"><i class="icon-edit icon-white"></i> Edit</button>
        </div>
    </fieldset>
</form>

The controller loads the bookmark model if the route param id is set, via REST. Note that the naming of the model in the controller just has to match the model naming of the view. The values will appear automatically in the view if a bookmark has been fetched. The snippet from the controller performs the loading task.

if ($routeParams.id) {
    $scope.bookmark = Rest.get({id: $routeParams.id});
}

AngularJS Bookmark App, edit view.
Editing a bookmark would look like this one.

list.html

I hope I did not bored you with the two views above, but I wanted to keep some of the coolest angular view features for the end. The ng-show and the ng-repeat directives. ng-show displays the current HTML element if the condition, which is the attribute value, is true. There is although a ng-hide which work similar to ng-show. This directive is used for displaying the bookmark title only if this value is set, otherwise the URL gets displayed in the link.

Like hiding or showing an element conditionally repeating can be done with ng-repeat, it repeats it's HTML element foreach like and work on arrays for example. Filtering and sorting can be done in a pipe like syntax with the ng-repeat directive, too. The value of the filter (filter:searchText) is a model defined in the search form above the bookmark list.

<table class="table table-striped table-condensed">
    <thead>
    <tr>
        <th colspan="2">
            <form class="form-horizontal">
                <input id="textinput" name="textinput" placeholder="Search" class="input-xlarge" type="text"
                       ng-model="searchText"/>
            </form>
        </th>
    </tr>
    </thead>
    <tbody id="bookmarks-list">
    <tr ng-repeat="bookmark in bookmarks | filter:searchText">
        <td>
            <a target="_blank" href="{{bookmark.url}}">
                <span ng-show="bookmark.title != ''">{{bookmark.title}}</span>
                <span ng-show="bookmark.title == ''">{{bookmark.url}}</span>
            </a>
        </td>
        <td>
            <div class="btn-group pull-right">
                <a class="btn btn-warning" href="#/edit/{{bookmark.id}}" title="edit">
                    <i class="icon-edit icon-white"></i></a>
                <a href="#" class="btn btn-danger" ng-click="delete(bookmark.id)" title="delete"><i
                        class="icon-trash icon-white"></i></a>
            </div>
        </td>
    </tr>
    </tbody>
</table>

AngularJS Bookmark App, list view.
This is the list of all bookmarks with the filtering form above.

going RESTful with ngResource

The ngResource module has the $resource factory which builds objects interacting with a RESTful web service. Going restful does not need the ngResource module, but it is much easier than using the lower level $http module. We have to create a factory in our module 'cacoBookMark.service' which uses ngResource and returns an object if the name matches the first param of the factory() method. The factory() methods second param is the closure creating our object.

The factory closure has the param $resource since it uses the ngResource, this is the factory we are using to build our RESTful object. The usage of the $resource factory is simple, it has three params the first it the URL to the REST back end, the second parameter is an object containing some parameter defaults and the third is an object containing the action definition. The action definition is straightforward, let's look at the remove method that performs a DELETE HTTP request on the URL 'api/bookmark/:id'. This snippet below is the factory in a factory building the REST service.

angular.module('cacoBookMark.service', ['ngResource']).factory('Rest', function ($resource) {
    return $resource('api/bookmark/:id', {}, {
        query:  {method: 'GET', isArray: true},
        get:    {method: 'GET'},
        remove: {method: 'DELETE'},
        edit:   {method: 'PUT'},
        add:    {method: 'POST'}
    });
});

Before we can start using the REST service called 'Rest' we have to ensure that the request sends from our service fits to the PHP server script which does not understand JSON as POST, which is the default method how the $http module sends requests if it is an object. The REST server wants the POST and PUT data to be classical URL query like encoded. This can be archived by configuring the $httpProvider, the transformRequest gets overridden with a method converting the flat object into a simple http query string. The defaults.headers for PUT and POST request has to be set to 'application/x-www-form-urlencoded; charset=UTF-8'. After this small adjustment, everything work fine and we can use the REST service now.

angular.module('cacoBookMark', ['cacoBookMark.service']).config(function ($httpProvider) {
    $httpProvider.defaults.transformRequest = function (data) {
        var str = [];
        for (var p in data) {
            data[p] !== undefined && str.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
        }
        return str.join('&');
    };
    $httpProvider.defaults.headers.put['Content-Type'] = $httpProvider.defaults.headers.post['Content-Type'] =
        'application/x-www-form-urlencoded; charset=UTF-8';
});

Controller

An AngularJS controller has a very simple structure, it's a pure JavaScript function with at least one parameter. This parameter should be named $scope. The $scope object gets initialized by the controller function which adds some models or actions to the scope. The example below shows a simple AngulaJS page just displaying "Hello World!", the SimpleCtrl controller just sets the value of the greeting model which has been used in the view. All models or actions accessed in a view belongs to the current controller scope.

<!DOCTYPE html>
<html ng-app>
    <head>
        <title>AngularJS in a nutshell</title>
    </head>
    <body ng-controller="SimpleCtrl">
        <p>{{greeting}}</p>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.4/angular.min.js"></script>
        <script>
        var SimpleCtrl = function ($scope) {
            $scope.greeting = 'Hello World!';
        };
        </script>
    </body>
</html>

AngularJS takes the controller function and injects all dependencies during initialization. The naming of the params is important, if you need access to the $location service, a controller function parameter has to be named $location. The order of the params is insignificant. The bookmark controller needs access to the $routeParams object, the $location service and our own Rest service, which has been discussed above. The parameter of the controller function reflects all the dependencies. What the controller does is simple, he loads a single bookmark if the route param id is set and if the location is equal to the root route he loads all bookmarks using the REST service. After loading the controller add the needed actions for adding, deleting or editing a bookmark. All those actions performing a REST operation and redirects to the root route if the action has been executed successfully.

var BookMarkCtrl = function ($scope, $routeParams, $location, Rest) {
    if ($routeParams.id) {
        $scope.bookmark = Rest.get({id: $routeParams.id});
    }
    if ($location.path() === '/') {
        $scope.bookmarks = Rest.query();
    }

    $scope.add = function () {
        Rest.add({}, $scope.newBookMark, function (data) {
            $location.path('/');
        });
    };

    $scope.delete = function (id) {
        if (!confirm('Confirm delete')) {
            return;
        }

        Rest.remove({id: id}, {}, function (data) {
            $location.path('/');
        });
    };

    $scope.save = function () {
        Rest.edit({id: $scope.bookmark.id}, $scope.bookmark, function (data) {
            $location.path('/');
        });
    };
};

Animation

Animations in AngularJS are new and available in the unstable version 1.1.4. The concept behind animating things in angular is using CSS3 transitions. I'm not going to write much about animations, the only animation I used is a simple fade in effect when a view gets loaded. You should take a look at the index.html file, the div containing the ng-view directive contains a ng-animate directive, too. On an enter event, the animation fade-enter gets used: ng-animate="{enter: 'fade-enter'}". This fade-enter animation has the two CSS classes fade-enter-setup and fade-enter-start, defined in the app.css file. fade-enter-setup adds a transition effect and sets the opacity to null/invisble on an enter event the class fade-enter-start gets added to the div element, which sets the opacity to one/full visible. Through the transition the opacity changes happen slowly and needs a half second. The CSS code is listed below.

body{
    padding-top: 80px;
}

.fade-enter-setup{
    -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
    -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
    -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
    -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
    transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
    opacity: 0;
}

.fade-enter-start{
    opacity: 1;
}

If you want to read more about AngularJS animations I suggest you this article from "year of moo" which is quite interesting.

The complete code

Maybe you want to read the entire code, which are just 57 lines, of the application.

angular.module('cacoBookMark.service', ['ngResource']).factory('Rest', function ($resource) {
    return $resource('api/bookmark/:id', {}, {
        query:  {method: 'GET', isArray: true},
        get:    {method: 'GET'},
        remove: {method: 'DELETE'},
        edit:   {method: 'PUT'},
        add:    {method: 'POST'}
    });
});

angular.module('cacoBookMark', ['cacoBookMark.service']).config(function ($httpProvider) {
    $httpProvider.defaults.transformRequest = function (data) {
        var str = [];
        for (var p in data) {
            data[p] !== undefined && str.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
        }
        return str.join('&');
    };
    $httpProvider.defaults.headers.put['Content-Type'] = $httpProvider.defaults.headers.post['Content-Type'] =
        'application/x-www-form-urlencoded; charset=UTF-8';
});

angular.module('cacoBookMark').config(['$routeProvider', function ($routeProvider) {
    $routeProvider.when('/add',      {templateUrl: 'partials/add.html',  controller: BookMarkCtrl})
                  .when('/edit/:id', {templateUrl: 'partials/edit.html', controller: BookMarkCtrl})
                  .when('/',         {templateUrl: 'partials/list.html', controller: BookMarkCtrl});
}]);

var BookMarkCtrl = function ($scope, $routeParams, $location, Rest) {
    if ($routeParams.id) {
        $scope.bookmark = Rest.get({id: $routeParams.id});
    }
    if ($location.path() === '/') {
        $scope.bookmarks = Rest.query();
    }

    $scope.add = function () {
        Rest.add({}, $scope.newBookMark, function (data) {
            $location.path('/');
        });
    };

    $scope.delete = function (id) {
        if (!confirm('Confirm delete')) {
            return;
        }

        Rest.remove({id: id}, {}, function (data) {
            $location.path('/');
        });
    };

    $scope.save = function () {
        Rest.edit({id: $scope.bookmark.id}, $scope.bookmark, function (data) {
            $location.path('/');
        });
    };
};

Conclusion

After getting into AngularJS I thought "what have I done before", which was writing hardly readable jQuery code. Doing a single page app in a structured and clean style and writing lesser code than using plain jQuery is great. A reason choosing angular, which has not been covered by this tutorial, is unit testing that is a strength by this framework, too. Maybe I should write more about how cool AngularJS is, but this would steal me the time playing more with this superheroic framework.

I hope you enjoyed reading my petty tutorial, the whole code is available at GitHub and a working demo can be found here here.