In this article I will discuss a better programming practice for Angular.js than was demonstrated in the previous articles within this series. I have mentioned before, part of the purpose this blog is very much a “learning in progress” for me. Without going through the previous articles I would not have been able to get to this point and write “better code”. Hopefully with that understanding, those of you who have been along for the ride will appreciate this and grow with me 🙂
Services within Angular
Within Angular there are these programmatic entities called services – and if you read around Angular programming practices (Follow @ToddMotto – http://toddmotto.com/opinionated-angular-js-styleguide-for-teams/) you will find (as I have) that adding application logic within Controllers is generally frowned upon.
“In AngularJS world, the services are singleton objects or functions that carry out specific tasks. It holds some business logic. Separation of concern is at the heart while designing an AngularJS application. Your controller must be responsible for binding model data to views using $scope. It does not contain logic to fetch the data or manipulating it.”
http://viralpatel.net/blogs/angularjs-service-factory-tutorial/
From a maintainability perspective, as well as logical code layout perspective, having functional logic within the controller is akin to writing a 1000 line LotusScript agent – not optimal !!
I used this article by Dan Whalin to better understand how to create a Service using a factory
So what changed?
We I changed the controller around so that code in the controller that once looked like this:
$scope.createPerson = function(event) { $http({ url: '//copper.xomino.com/xomino/ainx.nsf/api/data/documents?form=fUserName', data: $scope.person, withCredentials: true, method: "POST", headers: { "Content-Type": "application/json" } }) .success(function(data) { location.href = "#/people"; }) .error(function(data) { console.log('Error: ' + data); }); event.preventDefault(); return false; };
now looks like this:
$scope.createPerson = function(event) { dataFactory.createPerson($scope.person, event) .success(function(data) { event.preventDefault(); location.href = "#/people"; }) .error(function(data) { console.log('Error: ' + data); }); };
and the service looks like this:
dataFactory.createPerson = function (person, event) { console.log(person) return $http({ url: urlBase+'documents?form=fUserName', data: person, withCredentials: true, method: "POST", headers: { "Content-Type": "application/json" } }) };
Now I know you are looking at this and saying to yourself – hang on you just copied and pasted all the code and actually made it longer and more complicated – and that would be very perceptive of you – but (and I had to make this leap to believe it myself), it is better.
My example application only really has one purpose – manage people. In a more complex application a separation of logic into like components would make sense. By using the service I have separated all my “do stuff with people” out into a people Service. When I built the next part of the application – to do with buildings for example I would create a buildings Service and manage all my building logic there.
Let the controller do it’s job and bind the data to the View and keep your logic out of the controller and into Services.
The final Code
My service (service.js)
personApp.factory('dataFactory', ['$http', '$timeout', function($http, $timeout) { var urlBase = '//copper.xomino.com/xomino/ainx.nsf/api/data/'; var dataFactory = {}; dataFactory.getPeople = function () { return $http.get(urlBase+'collections/name/byFirstName5Col?open&count=3').success(function(data) { $scope.people = data; }); }; dataFactory.getPerson = function (id) { console.log('get person') return $http.get(urlBase+'documents/unid/' + id) }; dataFactory.createPerson = function (person, event) { console.log(person) return $http({ url: urlBase+'documents?form=fUserName', data: person, withCredentials: true, method: "POST", headers: { "Content-Type": "application/json" } }) }; dataFactory.savePerson = function (person, event, id) { console.log(id); return $http({ url: urlBase+'documents/unid/' + id, data: person, withCredentials: true, method: "PATCH", headers: { "Content-Type": "application/json" } }) }; dataFactory.deletePerson = function (id) { var temp = confirm('Are you sure you want to delete?') if (temp){ return $http.delete(urlBase+'documents/unid/' + id) } else { location.href = "#/people" } }; return dataFactory; }]);
My controller
peopleControllers.controller('PersonDetailCtrl', ['$scope', '$routeParams', '$http', 'action', '$timeout', 'dataFactory', function($scope, $routeParams, $http, action, $timeout, dataFactory) { $scope.create = (action=='new' || action=="delete"); if (action=="get") { dataFactory.getPerson($routeParams.docId) .success(function(data) { $scope.person = data; }) .error(function(data) { console.log('Error: ' + data); }); } $scope.home = function(){ location.href='#/people' }; $scope.createPerson = function(event) { dataFactory.createPerson($scope.person, event) .success(function(data) { event.preventDefault(); location.href = "#/people"; }) .error(function(data) { console.log('Error: ' + data); }); }; $scope.savePerson = function(event) { dataFactory.savePerson($scope.person, event, $routeParams.docId) .success(function(data) { event.preventDefault(); location.href = "#/people" }) .error(function(data, status, headers, config) { console.log('ErrorData: ' + data); }); }; if (action=="delete") { dataFactory.getPerson($routeParams.docId) .success(function(data) { $scope.person = data; $timeout(function(){ dataFactory.deletePerson($routeParams.docId) .success(function(data) { $scope.person = {}; location.href = "#/people" }) .error(function(data, status, headers, config) { console.log('ErrorData: ' + data); }); }, 300) }) .error(function(data) { console.log('Error: ' + data); }); }; }]);
Good job on the articles Mark.
We keep our controllers pretty much completely empty, all we use them for is to put “our controller object” (we struggled a bit with naming convention, for now we call it PeopleLogic) onto the scope, it has a service behind it. Rather than explain why (I’d do a terrible job explaining!) here are 2 good links, but a simple example is: Save and rather than Save and , where you’re not entirely sure where saveperson() is, especially if loads of DI. It also means that if you ever DI into something else, the “Controller” object you are passing has all the methods on it. So you might have a Company module and you inject your Person module into it, maybe you have some sort of ng-repeat grid that displays people at company, open a company in UI, it gets “people”, your DOM loads, and in your ng-repeat, you have Edit etc, you’re just calling method on the “our controller” person object. So you can run Person module in total isolation (e.g. CRUD on just people ), or inject into other modules (in your company module, your orders module…you have full CRUD and other methods on person in “Order”), uses same logic. Sweet.
I know you can also do “PersonController as Person”, blah, blah on Angular Controllers, but above seems cleaner.
Anyway, I could waffle on way too long about this, explanations below by people far more qualified than me.
It’s in here, Controller on steroids or something, but there’s a lot of good stuff in this one, enough to make my head start spinning.
And lots of good “best practice” stuff in here.
http://toddmotto.com/opinionated-angular-js-styleguide-for-teams/?utm_source=javascriptweekly&utm_medium=email
Cool. Very similar to what we have been doing for a long time. But this gives me more ideals to improve our process
[…] previous articles I have shown how to create a simple Angular.js application using a notes Domino Data Services feed […]
Don’t forget to change the “ng-click” event of the “Save” and “Create” buttons (add “$event” as parameter).
Modify the buttons html/tag of the “person.html” file as follow:
Save
Create
For the “Save” button: ng-click=”savePerson($event)” and
for the “Create” button: ng-click=”createPerson($event)”.
thanks 🙂