Tech tutorials Quick Guide to Angular Best Practices
By Insight Editor / 29 Jun 2015 , Updated on 16 May 2019 / Topics: Application development
By Insight Editor / 29 Jun 2015 , Updated on 16 May 2019 / Topics: Application development
If you were to search the internet for Angular best practices, you’d find a lot of resources. I found myself constantly searching for a multitude of things when I first started using Angular. While most of them worked, I kept looking at the code and thinking to myself, "There has to be a better way to do things," and, "What are other people doing?"
Through these searches, I found community leaders such as Todd Motto and John Papa and learned some lessons from them, as well as some lessons from my own experiences.
The goal of this synopsis is to empower those of you just starting with Angular and enable you to write clean, easy-to-maintain code without overwhelming you.
When starting a new application using Angular, it’s recommended to put all of the files related to the Angular application into their own folder. When creating our document structure, we should adhere to the LIFT principle:
Example:
Notice that by structuring the documents in our application like this, it becomes easy to find what we need. If the application gets larger, it may make sense to add another folder in the root that we might title “Pages” and then place our code for each route into its own folder from there, such as Pages > Home and Pages > Edit. This keeps our tree relatively flat and makes things easier to find as the amount of code increases.
When writing a large application using Angular, performance can be improved by minifying our JavaScript files. When I started off, a lot of examples I found showed the following to allow for minification of our JavaScript files. I would then structure the code by using return lines and tabs to try to increase its readability.
(function () { 'use strict'; angular.module('app').controller('MyController', ['$scope', function MyController($scope) { //Controller logic goes here }]); })();
It wasn’t until I came across John Papa’s style guide that I learned about the $inject service. At first, I didn’t see the benefit. The way I just mentioned worked, so why change it?
The reason for making the change comes down to readability. By using the $inject service, we can see how our controller becomes easier to understand for anyone who might look at our code. And it’s still safe to minify.
(function () { 'use strict'; angular.module('app').controller('MyController', MyController); MyController.$inject = ['$scope']; function MyController($scope) { //Controller logic goes here }; })();
Notice that the code looks much cleaner. We aren’t indented as much as the first method to minify our source code. It’s also much easier to ensure everything we need to inject is available, which can be hard to see using the first method as we have more dependencies.
When writing code in other languages, it can be very helpful to put the public functions at the top of our code and any private or helper methods below. The same holds true for JavaScript. Any functions in our controller that can be called from the Document Object Model (DOM) or any functions available to the controllers in our service should be at the top of our code.
This makes it easy at first glance to see what is usable in that section of code. If we also alphabetize the functions as we go down the page, it becomes very simple to find our code when we need to make changes to it.
Here’s an example of what not to do:
(function () { 'use strict'; angular.module('app').controller('MyController', MyController); MyController.$inject = ['$scope', 'MyDataSerice']; function MyController($scope, MyDataService) { $scope.busy = true; MyDataService.GetData() .then(function (result) { $scope.customers = result; }, function () { console.log("Data retrival failed"); }).finally(function () { $scope.busy = false; }); $scope.save = function (data) { MyDataService.SaveData(data) .then(function (result) { $scope.customers = result; }, function () { console.log("Data retrival failed"); }).finally(function () { $scope.busy = false; }); }; $scope.doSomethingElse = function (thing) { $scope.thing2 = thing; }; }; })();
This code would work, but it could make it difficult to determine what should occur during the page load and what functions can be called from the DOM. If we structure our code as exampled below, it becomes easier to follow.
(function () { 'use strict'; angular.module('app').controller('MyController', MyController); MyController.$inject = ['$scope', 'MyDataSerice']; function MyController($scope, MyDataService) { $scope.doSomethingElse = doSomethingElse; $scope.save = save; activate(); function activate() { $scope.busy = true; MyDataService.GetData() .then(function (result) { $scope.customers = result; }, function () { console.log("Data retrival failed"); }).finally(function () { $scope.busy = false; }); }; function doSomethingElse(thing) { $scope.thing2 = thing; }; function save(data) { MyDataService.SaveData(data) .then(function (result) { $scope.customers = result; }, function () { console.log("Data retrival failed"); }).finally(function () { $scope.busy = false; }); }; }; })();
Here’s an example of a service that follows the same guidelines and also stays DRY:
(function () { angular.module('myApp').factory('MyDataService', MyDataService); MyDataService.$inject = ['$http', '$q']; function MyDataService($http, $q) { return { getItems: getItems, searchItems: searchItems } function getItems () { var deffered = $q.defer(); $http.get('api/items/') .success(function (response) { var items = makeNewField(response); deffered.resolve(items); }).error(function (response) { deffered.reject(response); }); return deffered.promise; }; function makeNewField(items) { for (var i = 0; i < items.length; i++) { items[i].newField = "Field"; } return items; }; function searchItems() { var deffered = $q.defer(); $http.get('api/search/') .success(function (response) { var items = makeNewField(response); deffered.resolve(items); }).error(function (response) { deffered.reject(response); }); return deffered.promise; }; }; })();
Notice that by putting our return statement at the top, we can easily tell which functions are available for the controllers to call. It also alphabetizes the methods in the service, making them easier to find as the code is expanded.
Starting with the basics, remember this is still an application that’s being developed and, thus, we should adhere to separation of concerns. Our controller should only be responsible for updating our DOM data. Therefore, any data that needs to be retrieved should be a call to one of our data services.
If any other data needs to be saved into session storage or a cookie, this should also be handled in a data service as it doesn’t affect the DOM. If we’re using directives, each directive should have its own file. If the directive needs data from our server, the directive should have its data passed to it by the controller that retrieves the data from our data service.
Controller:
(function () { angular.module('myApp').factory('MyDataService', MyDataService); MyDataService.$inject = ['$http', '$q']; function MyDataService($http, $q) { return { getItems: getItems } function getItems () { var deffered = $q.defer(); $http.get('api/items/') .success(function(response) { deffered.resolve(response); }).error(function(response) { deffered.reject(response); }); return deffered.promise; } }; })();
Anything that has logic tied to it should be delegated to a service and not in a controller. The reason for this is to keep our code DRY. Rather than implement the same logic in multiple places, let the service handle it.
An example of when to do this is when looking up a variation ID of a product based on the color and size the user selected, as they all had the same source product ID. Rather than let the controller handle this logic, I separated it off to my product service and allowed the service to return the variation ID. This allowed the multiple controllers that relied on this logic to only be responsible for updating the data used in the DOM.