django+angular
— 22 min read

End to end web app with Django-Rest-Framework & AngularJS.

[Part 2].

Let’s pick up where we left off. If you haven’t already, make sure you go through part1 to create your base Django app with the Django REST Framework API setup.

Now, that we have our backend up and runing, we can get our hands dirty using AngularJs. We will create a simple one page app:

>  git checkout part2

Let’s create this one page served by our Django project.

 1 {% extends "base.html" %}
 2 {% load i18n %}
 3 {% block extra_head %}
 4     <meta name="keywords" content=""/>
 5     <meta name="description" content=""/>
 6     <script src="{{ STATIC_URL }}js/app/controllers/app-controller.js"></script>
 7     <script src="{{ STATIC_URL }}js/app/controllers/feed-controller.js"></script>
 8     <script src="{{ STATIC_URL }}js/app/controllers/posts-controller.js"></script>
 9     <script src="{{ STATIC_URL }}js/app/services/app-service.js"></script>
10     <script src="{{ STATIC_URL }}js/app/services/posts-service.js"></script>
11     <script src="{{ STATIC_URL }}js/app/directives/directives.js"></script>
12     <script src="{{ STATIC_URL }}js/app/filters/filters.js"></script>
13 {% endblock %}
14 {% block content %}
15     <div class="padded-content">
16         <div id="main-content" class="container">
17             <div class="row">
18                 <div class="span12">
19                     <div class="pending-bg" pendingbar>
20                         <div class="alert alert-warning">loading ...</div>
21                     </div>
22                 </div>
23              </div>
24                 <ng-view viewstate></ng-view>
25         </div>
26     </div>
27 {% endblock %}
28 {% block extra_content %}
29 <script type="text/javascript"></script>
30 {% endblock %}

As you can see, our “index” page is very simple, it inherits from a “base” page, and has two important features, or new html tags. This tags, directives in the angular language, extend our html abilities. We will go through this a bit later.

For now, let’s have a look at the structure of our angular app.

N.B: There are a couple of different ways we can add Angular into our application. In this project, I didn’t follow any particular one.

  • static
    • css
    • img
    • js
      • app
      • controllers
      • directives
      • filters
      • services
      • views
      • app.js
    • libs

My angular app will live under the folder app following this particular structure. At this point it is fairly simple, and should allow us to do basic interaction with the backend (create post, list posts, view a post by id, and edit a post).

App.js

First thing first, let’s start with the “app.js”. This file is important because it is there where we set some global settings, and set the routing of our app.

 1 'use strict';
 2 
 3 //(1)
 4 var Blog = angular.module("Blog", ["ui.bootstrap", "ngCookies"], function ($interpolateProvider) {
 5         $interpolateProvider.startSymbol("{[{");
 6         $interpolateProvider.endSymbol("}]}");
 7     }
 8 );
 9 
10 //(2)
11 Blog.run(function ($http, $cookies) {
12     $http.defaults.headers.common['X-CSRFToken'] = $cookies['csrftoken'];
13 })
14 
15 //(3)
16 Blog.config(function ($routeProvider) {
17     $routeProvider
18         .when("/", {
19             templateUrl: "static/js/app/views/feed.html",
20             controller: "FeedController",
21             resolve: {
22                 posts: function (PostService) {
23                     return PostService.list();
24                 }
25             }
26         })
27         .when("/post/:id", {
28             templateUrl: "static/js/app/views/view.html",
29             controller: "PostController",
30             resolve: {
31                 post: function ($route, PostService) {
32                     var postId = $route.current.params.id
33                     return PostService.get(postId);
34                 }
35             }
36         })
37         .otherwise({
38             redirectTo: '/'
39         })
40 })

(1): Since both Angular and Django use double braces, we need to set a new rule for Angular to separate between them. It’s not obligatory, but it is recommended. Especially if you want to get most of both frameworks.

We also add the dependency of ui.bootstrap and ngCookies.

(2): Sets the CSRF token, this needs angular-cookies.js

(3): routing for our app. We have two rules:

  1. “/” : pull the list of all posts
  2. “/post/:id” : the details of a particular post

You should notice that each view is delivered with controller. Also you can notice that we are using the resolve, to load data before changing the view. This have a better user experience, than changing the view and having to wait for the data to load.

Another thing to notice is that we don’t use routeParams,butinsteadweuseroute to access the params. Because $routeParams doesn’t get resolved until after the route is changed.

Directives

To make the user experience even better, we will create some directives.

Blog.directive('timeAgo', function ($timeout) {
    return {
        restrict: 'A',
        scope: {
            title: '@'
        },
        link: function (scope, elem, attrs) {
            var updateTime = function () {
                if (attrs.title) {
                    elem.text(moment(attrs.title).fromNow());
                    $timeout(updateTime, 15000);
                }
            };
            scope.$watch(attrs.title, updateTime);
        }
    };
});

Blog.directive('pendingbar', ['$rootScope',
    function ($rootScope) {
        return {
            link: function (scope, element, attrs) {
                element.addClass('hide');
                $rootScope.$on('$routeChangeStart', function () {
                    element.removeClass('hide');
                });
                $rootScope.$on('$routeChangeSuccess', function () {
                    element.addClass('hide');
                });
                $rootScope.$on('$routeChangeError', function () {
                    element.removeClass('hide');
                });
            }
        };
    }]);

Blog.directive('viewstate', ['$rootScope',
    function ($rootScope) {
        return {
            link: function (scope, element, attrs) {
                element.addClass('hide');
                $rootScope.$on('$routeChangeStart', function () {
                    element.addClass('hide');
                });
                $rootScope.$on('$routeChangeSuccess', function () {
                    element.removeClass('hide');
                });
                $rootScope.$on('$routeChangeError', function () {
                    element.addClass('hide');
                });
            }
        };
    }]);
  • timeAgo: Formats a date as the time since creation. What is particular without this directive is that it updats itself with refreshing the page.
  • pendingBar : This is a pending bar that shows when the user try to switch to another view.
  • viewState : This hides the view until the routing is completed.

Services

Our Angular part is going to access the data from our API using servieces. ngResource enables interation with RESTful server-side data sources, but in this post we will be using http and q.

Angular services are singletons that carry out specific tasks common to web apps. Services are commonly used to perform the XHR interaction with the server.

1 Blog.factory('GlobalService', function () {
2     var vars = {
3         is_authenticated: false
4     }
5 	return vars;
6 });

Our first service will allow to communicate between controllers and hold global variables. You can put some informations from the request [for example the context].

 1 Blog.factory('PostService', function ($http, $q) {
 2     var api_url = "/posts/";
 3     return {
 4         get: function (post_id) {
 5             var url = api_url + post_id + "/";
 6             var defer = $q.defer();
 7             $http({method: 'GET', url: url}).
 8                 success(function (data, status, headers, config) {
 9                     defer.resolve(data);
10                 })
11                 .error(function (data, status, headers, config) {
12                     defer.reject(status);
13                 });
14             return defer.promise;
15         },
16         list: function () {
17             var defer = $q.defer();
18             $http({method: 'GET', url: api_url}).
19                 success(function (data, status, headers, config) {
20                     defer.resolve(data);
21                 }).error(function (data, status, headers, config) {
22                     defer.reject(status);
23                 });
24             return defer.promise;
25         },
26         update: function (post) {
27             var url = api_url + post.id + "/";
28             var defer = $q.defer();
29             $http({method: 'PUT',
30                 url: url,
31                 data: post}).
32                 success(function (data, status, headers, config) {
33                     defer.resolve(data);
34                 }).error(function (data, status, headers, config) {
35                     defer.reject(status);
36                 });
37             return defer.promise;
38         },
39         save: function (post) {
40             var url = api_url;
41             var defer = $q.defer();
42             $http({method: 'POST',
43                 url: url,
44                 data: post}).
45                 success(function (data, status, headers, config) {
46                     defer.resolve(data);
47                 }).error(function (data, status, headers, config) {
48                     defer.reject(status);
49                 });
50             return defer.promise;
51         },
52     }
53 });

The second service, is an object for accessing a Post object of our REST API.

Controllers

For this app, we create 3 controllers.

A controller to initialize our app. This is a global controller that we put at the beginning of our app.

 1 var appController = Blog.controller('AppController', function ($scope, $rootScope, $location, GlobalService) {
 2     var failureCb = function (status) {
 3         console.log(status);
 4     };
 5     $scope.globals = GlobalService;
 6 
 7     $scope.initialize = function (is_authenticated) {
 8         $scope.globals.is_authenticated = is_authenticated;
 9     };
10 })
1 <body data-spy="scroll" id="index" style="zoom: 1;" ng-controller="AppController" ng-init="initialize('')">

A controller to manage the list of posts, and create new ones

 1 Blog.controller('FeedController', function ($scope, GlobalService, PostService, posts) {
 2     $scope.posts = posts;
 3     $scope.globals = GlobalService;
 4     //options for modals
 5     $scope.opts = {
 6         backdropFade: true,
 7         dialogFade: true
 8     };
 9     //open modals
10     $scope.open = function (action) {
11         if (action === 'create'){
12             $scope.postModalCreate = true;
13             $scope.post = new Object();
14         };
15     };
16     //close modals
17     $scope.close = function (action) {
18         if (action === 'create'){
19             $scope.postModalCreate = false;
20         };
21     };
22     //calling board service
23     $scope.create = function () {
24         PostService.save($scope.post).then(function (data) {
25             $scope.post = data;
26             $scope.posts.push(data);
27             $scope.postModalCreate = false;
28         }, function(status){
29             console.log(status);
30         });
31     };
32 });

A controller to view and edit a single post

 1 Blog.controller('PostController', function ($scope, $routeParams, $location, PostService, GlobalService, post) {
 2     $scope.post = post;
 3     $scope.globals = GlobalService;
 4     var failureCb = function (status) {
 5         console.log(status);
 6     }
 7     //options for modals
 8     $scope.opts = {
 9         backdropFade: true,
10         dialogFade: true
11     };
12     //open modals
13     $scope.open = function (action) {
14         $scope.postName = $scope.post.name;
15         $scope.postDescription = $scope.post.description;
16         if (action === 'edit'){
17             $scope.postModalEdit = true;
18         };
19     };
20     //close modals
21     $scope.close = function (action) {
22         $scope.postName = "";
23         $scope.postDescription = "";
24         if (action === 'edit'){
25             $scope.postModalEdit = false;
26         };
27     };
28     //calling board service
29     $scope.update = function () {
30         PostService.update($scope.post).then(function (data) {
31             $scope.post = data;
32             $scope.postModalEdit = false;
33         }, failureCb);
34     };
35 });

filters

For aesthetic purposes, we create a simple filter to truncate the posts’ description.

 1 Blog.filter('truncate', function () {
 2         return function (text, length, end) {
 3             if (isNaN(length))
 4                 length = 10;
 5 
 6             if (end === undefined)
 7                 end = "...";
 8 
 9             if (text.length <= length || text.length - end.length <= length) {
10                 return text;
11             }
12             else {
13                 return String(text).substring(0, length-end.length) + end;
14             }
15 
16         };
17     });

Views

We need 2 views:

feed.html : List of posts and creation of new posts

 1 <br>
 2 <div class="row">
 3     <div class="span9 main">
 4         <div class="row">
 5             <div class="span9">
 6                 <a class="btn" ng-click="open('create')"> Create</a>
 7 
 8                 <div modal="postModalCreate" close="close('create')" options="opts">
 9                     <a ng-click="close('create')" class="close" data-dismiss="modal">×</a>
10 
11                     <div class="modal-header">
12                         <h4>Edit tip</h4>
13                     </div>
14                     <div class="modal-body">
15                         <form class="form-horizontal">
16                             <div class="control-group">
17                                 <label class="control-label" for="id_title">Title</label>
18 
19                                 <div class="controls">
20                                     <input type="text" id="id_title" placeholder="Title" ng-model="post.title">
21                                 </div>
22                             </div>
23                             <div class="control-group">
24                                 <label class="control-label" for="id_description">Description</label>
25 
26                                 <div class="controls">
27                                     <textarea type="text" id="id_description"
28                                               ng-model="post.description"></textarea>
29                                 </div>
30                             </div>
31                         </form>
32                     </div>
33                     <div class="modal-footer">
34                         <button class="btn btn-success" ng-click="create()">create</button>
35                         <button class="btn btn-danger" ng-click="close('create')">Cancel</button>
36                     </div>
37                 </div>
38             </div>
39         </div>
40         <div class="row" ng-repeat="post in posts">
41             <div class="span9">
42                 <div class="bg"></div>
43                 <div class="overlay">
44                     <div class="content">
45                         <a class="clip-open" href="{[{post.api_url}]}">
46                             <h3>{[{post.title}]}</h3>
47                         </a>
48 
49                         <div class="meta clips-note">
50                             <p>
51                                 {[{post.description|truncate:150}]}
52                             </p>
53                         </div>
54                         <small><abbr time-ago title="{[{post.created_on}]}" class="date"></abbr></small>
55                     </div>
56                 </div>
57             </div>
58         </div>
59     </div>
60 </div>

view.html : View and update a single post.

 1 <div class="row">
 2     <div class="span9 main">
 3         <div class="row">
 4             <div class="span9">
 5                 <h3>{[{ post.title }]}</h3>
 6                 <p>
 7                     {[{ post.description }]}
 8                 </p>
 9             </div>
10         </div>
11         <div class="row">
12             <div class="span9">
13                 <a class="btn" ng-click="open('edit')"> Edit</a>
14                 <a class="btn" href="#/"> Go back</a>
15                 <div modal="postModalEdit" close="close('create')" options="opts">
16                     <a ng-click="close('edit')" class="close" data-dismiss="modal">×</a>
17 
18                     <div class="modal-header">
19                         <h4>Edit tip</h4>
20                     </div>
21                     <div class="modal-body">
22                         <form class="form-horizontal">
23                             <div class="control-group">
24                                 <label class="control-label" for="id_title">Title</label>
25 
26                                 <div class="controls">
27                                     <input type="text" id="id_title" placeholder="Title" ng-model="post.title">
28                                 </div>
29                             </div>
30                             <div class="control-group">
31                                 <label class="control-label" for="id_description">Description</label>
32 
33                                 <div class="controls">
34                                     <textarea type="text" id="id_description"
35                                               ng-model="post.description"></textarea>
36                                 </div>
37                             </div>
38                         </form>
39                     </div>
40                     <div class="modal-footer">
41                         <button class="btn btn-success" ng-click="update()">Update</button>
42                         <button class="btn btn-danger" ng-click="close('edit')">Cancel</button>
43                     </div>
44                 </div>
45             </div>
46         </div>
47     </div>
48 </div>

In the next parts, we’ll add permissions, tags, sorting, searching, and pagination.