AngularJS Best Practices:
The Rule Of 1

Angular Best Practices: The Rule Of 1

Angular 1.x has been around over several years now and since it’s become one of the most popular frameworks out there, you might be wondering if any best practices have evolved over time. If you are considering developing your first angular application and what to know what the potential pitfalls would be but unsure where to look, we plan to tackle some of those. In this series, we are going to cover the best practices when developing your Angularjs applications. We’ll take from various popular style guides and in this article, we will start with John Papa’s.

 

When developing angular apps, we define controllers, templates, routers, directives and components. While it’s possible to put all of your js logic inside one file and bundle all of it together, you are actually making it more difficult to maintain and read over the long run. Think about the evolution of your project. At first it is probably a simple and concise app but over time as more logic and complexity is introduced, your files becomes extremely large and bundle all sorts of app logic inside one location. Plus, if you are working in a team environment, you are probably dealing with tons of merge issues (think github). Not only are you having all of this fun stuff going on but you are actually increasing the potential for collisions when making commits to your repo with fellow developers. Additionally, by coupling all of these different angular artifacts into a single file, you may be potentially coupling unnecessarily dependencies with shared variables, injections and so on. And Finally, there’s the difficulty of unit testing your angular artifacts.

       Rule of 1

       Rule of 1

How do we solve this?

Rule of 1

  • Define 1 component per file, recommended to be less than 400 lines of code.

  • Why?: One component per file promotes easier unit testing and mocking.

  • Why?: One component per file makes it far easier to read, maintain, and avoid collisions with teams in source control.

  • Why?: One component per file avoids hidden bugs that often arise when combining components in a file where they may share variables, create unwanted closures, or unwanted coupling with dependencies
     

Let’s take a look at an example of applying the Rule of 1. 

angular.module('todo', []);
 
.controller('TodoCtrl', function($scope) {
  
  $scope.todoText = "";
  
  $scope.todos = [
    {text:'Project Setup', done:true},
    {text:'AngularJS Basics', done:false},
    {text:'Simple Todo Application', done:false}
  ];

  $scope.addTodo = function() {
    $scope.todos.push({text:$scope.todoText, done:false});
    $scope.todoText = '';
  };
  
});

In this application, notice how all the logic is contained inside a single js file. We have coupled our controllers, services and directives into a single location.

Example:

We start by applying the rule with separating our bundled “components’ into single separated components.

Doing this would imply the following file structure:

/todo

todo.controller.js
todo.directive.js
todo.services.js
todo.module.js

Notice in this example it’s clear by simply looking at the name of what the purpose of each individual file is for. Notice the name prefix. Just by reading the name “todo.controller.js”, you get the idea that you are dealing with a controller for a todo module.

Next, we split out definitions in each of these files and use dependency injection to link them together.

//todo.module.js

angular.module('todo', ['ngRoute']);

 

 

Next, we define our controller method in its own file:

//todo.controller.js

angular(‘todo’)

.controller('TodoCtrl', [ ‘scope’, ‘TodoSrv’, function($scope, todoSrv) {

$scope.todoText = "";
$scope.todos = [
   {text:'Project Setup', done:true},
   {text:'AngularJS Basics', done:false},
   {text:'Simple Todo Application', done:false}
 ];

 $scope.addTodo = function() {

   $scope.todos.push({text:$scope.todoText, done:false});

   $scope.todoText = '';
 };
});

 

Finally, we define our services into a separate file

//todo.services.js

angular.module('todo')
.factory(‘TodoSrv’. function(){

});

To demonstrate the flexibility of this approach, let’s introduce a new problem. Suppose a new feature is being developed called Memo’s. One of the features is to display the todo services data in a different visual representation.

Because we have separated the files out, we can leverage dependency injection to handle wiring our dependency externally (in separate files) by simply using dependency injection (DI). Now, we make the service available to our new feature by including it:

//memo.controller.js

angular.
controller('MemoCtrl', [ ‘scope’, ‘TodoSrv’, function($scope, todoSrv) {
    $scope.todos = todoSrv.getTodos();
});

 

Now, suppose that the underlying service contract is being changed by adding a new additional field. Suppose we want to be able to categorize our todo’s but still support our legacy systems using the existing todo’s implementation.

We can modify our backend service to return a new field ‘category’ which bubbles up to the UI layer.  Our newly updated MemoCtrl will observe the property and act accordingly, yet all the consumers of the existing application will go unaffected by the its introduction.

//memo.controller.v2.js

angular.
controller('MemoCtrl', [ ‘scope’, ‘TodoSrv’, function($scope, todoSrv) {
    $scope.memos = todoSrv.getTodos();
});
angular.
controller('MemoCtrl', [ ‘scope’, ‘TodoSrv’, function($scope, todoSrv) {
    $scope.memos = todoSrv.getTodos();
});

//TODO.CONTROLLER.JS
angular(‘todo’)
.controller('TodoCtrl', [ ‘scope’, ‘TodoSrv’, function($scope, todoSrv) {
$scope.todos = todoSrv.getTodos();  
}]);

//TODO.SERVICES.JS
angular.
module('todo')
.factory(‘TodoSrv’. function(){

    return {
    getTodos: {
      //mock service call
        return [{
            ‘category’ :  ‘Tasks’, //new field
             ‘todo'    :     ‘Do Laundary’,
             ‘done’    :  ‘Nope’        
                
         }]    

    }

  };    
});

 

We now have multiple controllers using the same underlying services but have a clean separation of concern. This meant that we only had to modify one file while all of the existing service consumers did not need to be changed. If all the logic was developed in a single file, we would have had to create duplicate services or implement a shared service to handle these different features instead of using a dependency injection to handle this for us.

Benefits:

By using the rule of 1, each individual file represents a single functional component or unit which when assembled together creates our application.

By using the rule of 1, it’s clear what each file’s purpose is and what the intent of it is.

By using the rule of 1, our applications components are more flexible and allow for more change.

By using the rule of 1, three team members could be working simultaneously on three different files without having to deal with merges. One for the memo feature, one for the todo feature and one backend developer for the service.

Note that in this example no one is working on the actual TodoSrv angular factory and yet it handles it automatically!!