AngularJS: Introducing modules, controllers, services
In my previous post AngularJS Tutorial: Getting Started with AngularJS we have seen how to setup an application using SpringBoot + AngularJS + WebJars. But it’s a kind of quick start tutorial where I haven’t explained much about AngularJS modules, controllers and services. Also it is a single screen (only one route) application.
In this part-2 tutorial, we will take a look at what are Angular modules, controllers and services and how to configure and use them. Also we will look into how to use ngRoute to build multi-screen application.
If we take a look at the code that we developed in previous post, especially in controllers.js, we clubbed the client side controller logic and business logic(of-course we don’t have any biz logic here !) in our Controllers which is not good.
As java developers we get used to have dozen layers and we love making things complex and complain Java is complex. But here in AngularJS things looks simpler, let’s make things little bit complex. I am just kidding !
Even if you put all your logic in single place as we did in controllers.js, it will work and acceptable for simple applications. But if you are going to develop large enterprise application (who said enterprise applications should be large…hmm..ok..continue..) then things quickly become messy. And believe me working with a messy large JavaScript codebase is lot more painful than messy large Java codebase. So it is a good idea to separate the business logic from controller logic.
In AngularJS we can organize application logic into modules and make them work together using dependency injection. Lets see how to create a module in AngularJS.
var myModule = angular.module('moduleName',['dependency1','dependency2']);
This is how we can create a module by using angular.module() function by passing the module name and specifying a list of dependencies if there are any.
Once we define a module we can get handle of the module as follows:
var myModule = angular.module('moduleName');
Observe that there is no second argument here which means we are getting the reference of a predefined angular module. If you include the second argument, which is an array, then it means you are defining the new module.
Once we define a new module we can create controllers in that module as follows:
module.controller('ControllerName',['dependency1','dependency2', function(dependency1, dependency2){ //logic }]);
For example, lets see how we to create TodoController.
var myApp = angular.module('myApp',['ngRoute']); myApp.controller('TodoController',['$scope','$http',function($scope,$http){ //logic }]);
Here we are creating TodoController and providing $scope and $http as dependencies which are built-in angularjs services.
We can also create the same controller as follows:
myApp.controller('TodoController',function($scope,$http){ //logic });
Observe that we are directly passing a function as a second argument instead of an array which has an array of dependencies followed by a function which takes the same dependencies as arguments and it works exactly same as array based declaration.
But why do we need to do more typing when both do the same thing??
AngularJS injects the dependencies by name, that means when you define $http as a dependency then AngularJS looks for a registered service with name ‘$http‘. But majority of the real world applications use JavaScript code minification tools to reduce the size. Those tools may rename your variables to short variable names.
For example:
myApp.controller('TodoController',function($scope,$http){ //logic });
The preceding code might be minified into:
myApp.controller('TodoController',function($s,$h){ //logic });
Then AngularJS tries to look for registered services with names $s and $h instead of $scope and $http and eventually it will fail. To overcome this issue we define the names of services as string literals in array and specify the same names as function arguments. With this even after JavaScript minifies the function argument names, string literals remains same and AngularJS picks right services to inject.
That means you can write the controller as follows:
myApp.controller('TodoController',['$scope','$http',function($s,$h){ //here $s represents $scope and $h represents $http services }]);
So always prefer to use array based dependencies approach.
Ok, now we know how to create controllers. Lets see how we can add some functionality to our controllers.
myApp.controller('TodoController',['$scope','$http',function($scope,$http){ var todoCtrl = this; todoCtrl.todos = []; todoCtrl.loadTodos = function(){ $http.get('/todos.json').success(function(data){ todoCtrl.todos = data; }).error(function(){ alert('Error in loading Todos'); }); }; todoCtrl.loadTodos(); }]);
Here in our TodoController we defined a variable todos which initially holds an empty array and we defined loadTodos() function which loads todos from RESTful services using $http.get() and once response received we are setting the todos array to our todos variable. Simple and straight forward.
Why can’t we directly assign the response of $http.get() to our todos variable like todoCtrl.todos = $http.get(‘/todos.json’);??
Because $http.get(‘/todos.json’) returns a promise, not actual response data. So you have to get data from success handler function. Also note that if you want to perform any logic after receiving data from $http.get() you should put your logic inside success handler function only.
For example if you are deleting a Todo item and then reload the todos you should NOT do as follows:
$http.delete('/todos.json/1').success(function(data){ //hurray, deleted }).error(function(){ alert('Error in deleting Todo'); }); todoCtrl.loadTodos();
Here you might assume that after delete is done it will loadTodos() and the deleted Todo item won’t show up, but that won’t work like that. You should do it as follows:
$http.delete('/todos.json/1').success(function(data){ //hurray, deleted todoCtrl.loadTodos(); }).error(function(){ alert('Error in deleting Todo'); });
Lets move on to how to create AngularJS services. Creating services is also similar to controllers but AngularJS provides multiple ways for creating services.
There are 3 ways to create AngularJS services:
- Using module.factory()
- Using module.service()
- Using module.provider()
Using module.factory()
We can create a service using module.factory() as follows:
angular.module('myApp') .factory('UserService', ['$http',function($http) { var service = { user: {}, login: function(email, pwd) { $http.get('/auth',{ username: email, password: pwd}).success(function(data){ service.user = data; }); }, register: function(newuser) { return $http.post('/users', newuser); } }; return service; }]);
Using module.service()
We can create a service using module.service() as follows:
angular.module('myApp') .service('UserService', ['$http',function($http) { var service = this; this.user = {}; this.login = function(email, pwd) { $http.get('/auth',{ username: email, password: pwd}).success(function(data){ service.user = data; }); }; this.register = function(newuser) { return $http.post('/users', newuser); }; }]);
Using module.provider()
We can create a service using module.provider() as follows:
angular.module('myApp') .provider('UserService', function() { return { this.$get = function($http) { var service = this; this.user = {}; this.login = function(email, pwd) { $http.get('/auth',{ username: email, password: pwd}).success(function(data){ service.user = data; }); }; this.register = function(newuser) { return $http.post('/users', newuser); }; } } });
You can find good documentation on which method is appropriate in which scenario at http://www.ng-newsletter.com/advent2013/#!/day/1.
Let us create a TodoService in our services.js file as follows:
var myApp = angular.module('myApp'); myApp.factory('TodoService', function($http){ return { loadTodos : function(){ return $http.get('todos'); }, createTodo: function(todo){ return $http.post('todos',todo); }, deleteTodo: function(id){ return $http.delete('todos/'+id); } } });
Now inject our TodoService into our TodoController as follows:
myApp.controller('TodoController', [ '$scope', 'TodoService', function ($scope, TodoService) { $scope.newTodo = {}; $scope.loadTodos = function(){ TodoService.loadTodos(). success(function(data, status, headers, config) { $scope.todos = data; }) .error(function(data, status, headers, config) { alert('Error loading Todos'); }); }; $scope.addTodo = function(){ TodoService.createTodo($scope.newTodo). success(function(data, status, headers, config) { $scope.newTodo = {}; $scope.loadTodos(); }) .error(function(data, status, headers, config) { alert('Error saving Todo'); }); }; $scope.deleteTodo = function(todo){ TodoService.deleteTodo(todo.id). success(function(data, status, headers, config) { $scope.loadTodos(); }) .error(function(data, status, headers, config) { alert('Error deleting Todo'); }); }; $scope.loadTodos(); }]);
Now we have separated our controller logic and business logic using AngularJS controllers and services and make them work together using Dependency Injection.
In the beginning of the post I said we will be developing a multi-screen application demonstrating ngRoute functionality.
In addition to Todos, let us add PhoneBook feature to our application where we can maintain list of contacts.
First, let us build the back-end functionality for PhoneBook REST services.
Create Person JPA entity, its Spring Data JPA repository and Controller.
@Entity public class Person implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.AUTO) private Integer id; private String email; private String password; private String firstname; private String lastname; @Temporal(TemporalType.DATE) private Date dob; //setters and getters } public interface PersonRepository extends JpaRepository<Person, Integer>{ } @RestController @RequestMapping("/contacts") public class ContactController { @Autowired private PersonRepository personRepository; @RequestMapping("") public List<Person> persons() { return personRepository.findAll(); } }
Now let us create AngularJS service and controller for Contacts. Observe that we will be using module.service() approach this time.
myApp.service('ContactService', ['$http',function($http){ this.getContacts = function(){ var promise = $http.get('contacts') .then(function(response){ return response.data; },function(response){ alert('error'); }); return promise; } } }]); myApp.controller('ContactController', [ '$scope', 'ContactService', function ($scope, ContactService) { ContactService.getContacts().then(function(data) { $scope.contacts = data; }); } ]);
Now we need to configure our application routes in app.js file.
var myApp = angular.module('myApp',['ngRoute']); myApp.config(['$routeProvider','$locationProvider', function($routeProvider, $locationProvider) { $routeProvider .when('/home', { templateUrl: 'templates/home.html', controller: 'HomeController' }) .when('/contacts', { templateUrl: 'templates/contacts.html', controller: 'ContactController' }) .when('/todos', { templateUrl: 'templates/todos.html', controller: 'TodoController' }) .otherwise({ redirectTo: 'home' }); }]);
Here we have configured our application routes on $routeProvider inside myApp.config() function.
When url matches with any of the routes then corresponding template content will be rendered in <div ng-view></div> div in our index.html.
If the url doesn’t match with any of the configured urls then it will be routed to ‘home‘ as specified in otherwise() configuration.
Our templates/home.html won’t have anything for now and templates/todos.html file will be same as home.html in previous post.
The new templates/contacts.html will just have a table listing contacts as follows:
<table class="table table-striped table-bordered table-hover"> <thead> <tr> <th>Name</th> <th>Email</th> </tr> </thead> <tbody> <tr ng-repeat="contact in contacts"> <td>{{contact.firstname + ' '+ (contact.lastname || '')}}</td> <td>{{contact.email}}</td> </tr> </tbody> </table>
Now let us create navigation links to Todos, Contacts pages in our index.html page <body>.
<div class="container"> <div class="row"> <div class="col-md-3 sidebar"> <div class="list-group"> <a href="#home" class="list-group-item"> <i class="fa fa-home fa-lg"></i> Home </a> <a href="#contacts" class="list-group-item"> <i class="fa fa-user fa-lg"></i> Contacts </a> <a href="#todos" class="list-group-item"> <i class="fa fa-indent fa-lg"></i> ToDos </a> </div> </div> <div class="col-md-9 col-md-offset-3"> <div ng-view></div> </div> </div> </div>
By now we have a multi-screen application and we understood how to use modules, controllers and services.
You can find the code for this article at https://github.com/sivaprasadreddy/angularjs-samples/tree/master/angularjs-series/angularjs-part2
Our next article would be on how to use $resource instead of $http to consume REST services.
Also we will look update our application to use more powerful ui-router module instead of ngRoute. Stay tuned !
Reference: | AngularJS: Introducing modules, controllers, services from our JCG partner Siva Reddy at the My Experiments on Technology blog. |
There is clear explanation . Thanks. Hope you’ll continue your experiments with AngularJS.
My next article on AngularJS Filters published. Take a look http://www.sivalabs.in/2014/09/angularjs-different-ways-of-using-array.html :-)
I would glad to know, how we are maintaining directory structure in eclipse for this project.
If you share yours one then it would really help me.
I developed one spring app, where I am maintaining my view pages under WEB-INF/jsp and I have configured same in spring-servlet.xml and in angularjs routing I am trying to point templateUrl to view pages under WEB-INF/jsp but it is giving me 404-Error.
So you need your guidance on this.
Why did we create TodoService with factory and ContactService with service? what is the difference between them?