Angular: Best Practices

Directory Structure

Group by feature, not by function

Components

app.js

components/
            users/
                 users-service.js
                 users-directive.js
                 users-controller.js
                 users.scss
                 users.html

            version/
                 version-directive.js
                 version-controller.js
                 version.scss
                 version.html

use the generator.

Dependency Injection

Be Explicit, @#$&! 

Many ways to D.I.

angular.module('myApp', [])
    .factory(MyFactory);

function MyService($dep1, dep2, dep3) { ... }
angular.module('myApp', [])
    .factory(['$dep1', 'dep2', 'dep3', MyService]);

function MayService($dep1, dep2, dep3) { ... }
angular.module('myApp', [])
    .factory(MyService);

MyService.$inject = ['$dep1', 'dep2', 'dep3']; //Function properties as decorators?
function MyService($dep1, dep2, dep3) { ... }

the "right" way 

Alleviating the Tedium

angular.module('myApp', [])
    .factory(MyService);

function MyService($dep1, dep2, dep3) { 
'ngInject';
...
}

MWS uses the ng-annotate gulp plugin

Identify functions that need annotations with the 'ngInject'; prologue

use the generator!

Directives

Restrict directives to elements or attributes

app.directive('myComponent', myComponent);

function myComponent() {
    return {
        ...
        restrict: 'EA'
    };
}

Only directives should manipulate DOM and only in a directive's `link` function.

app.directive('myComponent', myComponent);

function myComponent() {
    return {
        ...
        link: linkFn
    };

    function linkFn(scope, element, attributes) {
        //DOM manipulation here only!
    }
}

Aviod $scope.$watch at ALL costs.

app.directive('myComponent', myComponent);

function myComponent() {
    return {
        ...
        link: linkFn
    };

    function linkFn() {
        scope.$watch('title', function (prevVal, currVal) {
            //this is super taxing and will bog down your application
        }); 
    }
}

Use native event handlers instead.

app.module('my-app')
    .controller('myCompCtrl', myCompCtrl);

function myCompCtrl() {
        
    return {
        onChange: function (evt) {
            // react to change.
       }
    };
}

//my-component.html template

<input type="text" ng-change="onChange" />

React to changes in the directive's controller.

app.module('my-app')
    .directive('myComp', myComp);

function myComp() {
        
    return {
        restrict: 'EA',
        controller: myCompCtrl,
        controllerAs: 'myCompCtrl'
        bindToController: true,
        link: linkFn,
       }
    };
}

Watchers

are $evil

Watchers are the methamphetamine of any angular application. It serves a purpose, but it's highly addictive and extremely damaging. The worst part is, out of the box, Angular acts more like a dealer than a pharmacist.

 

$digest is the problem. The $digest cycle is a loop over all your bindings that checks for changes and re-renders accordingly.

 

Bindings happen automatically not only for `{{ }}` but expressions including ng-if, ng-class, and most damning, ng-repeat 

Your app has a fever, and the only prescription is...

One time binding

Angular 1.3 introduced a syntax for binding a value only once. This should be the default, and in fact, in Angular 2 it is.

Until then, ALWAYS use one time binding.

{{:: myCtrl.user }}

<div ng-if="::myCtrl.user"></div>

<div ng-class="::{ user: myCtrl.user }"></div>

<ol>
    <li ng-repeat="user in ::myCtrl.users"></li>
</ol>

$scope

is $evil

$scope is an inheritance nightmare

Scopes are arranged in hierarchical structure which mimic the DOM structure of the application

​- https://docs.angularjs.org/guide/scope

OMG wat?!?!?!

That tots sounds like a good idea.

<div class="show-scope-demo" ng-controller="GreetController">
  
  <h1>Hello {{name}}</h1>

  <div ng-controller="ListController">
      <ol>
          <li ng-repeat="name in names">
            {{name}} from {{department}} says hi to {{$parent.name}}
          </li>
      </ol>
  </div>
</div>

OMG! (Oh my gross!)

Just. Why? When you don't have to?

It gets worse

<div ng-controller="MainCtrl">
  {{ title }}
  <div ng-controller="AnotherCtrl">
    Scope title: {{ title }}
    MainCtrl's title: {{ $parent.title }}
    <div ng-controller="YetAnotherCtrl">
      {{ title }}
      YetAnotherCtrl's title: {{ $parent.title }}
      MainCtrl's title: {{ $parent.$parent.title }}
    </div>
  </div>
</div>

Each nested controller has to have knowledge of controller(s) it's composed with and at what depth.

controllerAs FTLHL

<div ng-controller="MainCtrl as main">
  {{ main.title }}
  <div ng-controller="AnotherCtrl as another">
    Scope title: {{ another.title }}
    Parent title: {{ main.title }}
    <div ng-controller="YetAnotherCtrl as yet">
      Scope title: {{ yet.title }}
      Parent title: {{ another.title }}
      Parent parent title: {{ main.title }}
    </div>
  </div>
</div>

Child controllers can now reference parent controllers by their namespace, rather than through some convoluted $parent reference N times.

(For The Less Humiliating Loss)

controllerAs FTLHL

app.controller('MainCtrl', MainCtrl);

MainCtrl.$inject = [];

function MainCtrl() {
  this.title = 'Some title';
}

We're also able to treat our controller as a constructor and attach our context to this

(For The Less Humiliating Loss)

WARNING!!!

app.controller('MainCtrl', MainCtrl);

MainCtrl.$inject = [];

function MainCtrl() {

    return {
        title: 'Some title'    
    };
}

We could additionally compel our controller to act like a factory -- the way the rest of Angular methods do -- by returning an object. 

Controversy ahead!!! Not advised for those with low blood pressure, those who are pregnant or may  become pregnant, and the elderly.

Route controllerAs FTD

.when('/', {
    templateUrl: 'components/main/main.html',
    controllerAs: 'main',
    controller: 'MainCtrl'
});

Better right? Our templates are more reusable, our controllers are more reusable, and we've separated our concerns much better.

(For The Draw)

FOUL!!!!!!

You can't do nested views with controllerAs in your route definition!

Nested Views

are $evil

Stop thinking of your application in terms of views that need to be loaded. That kind of thinking aligns better with imperative frameworks but doesn't work well in angular.

- Jan Varwig

An Angular 2 application consists of nested components. So an Angular 2 application will always have a component tree.

- Victor Savkin: core Angular 2 developer

MVC

MV

MVVM

MV*

MC

Models

Components

App developers are responsible for updating models.

Angular reflects model changes into UI via components

Models

Components

Services,

Filters,

Factories

Views,

Directives,

Controllers

Components

component = directive + controller

app.directive('myComponent', myComponent);

function myComponent() {
    return {
        restrict: 'EA',
        scope: true,
        templateUrl: 'my-component.html',
        controllerAs: 'comp', 
        controller: MyComponentCtrl
        bindToController: true,
        link: linkFn
    };
}

Views

view = [[component]]

.when('/', {
    templateUrl: 'views/main.html'
  })
.when('/', {
    template: '<my-home></my-home>'
  })

Controllers

Controllers should only be for creating view-models

The idea here is to avoid logic in your controller.

 

The effect of this is that virtually all functions that you may be tempted to write in a controller, should instead by put into a service.

 

This also simplifies controller code and clarifies the role of a controller.

angular.module('my-app')
    .controller('myCtrl', MyCtrl);

function MyCtrl() {
    //cache our reference
    var vm = this;
    
    //This is cool
    vm.firstName = 'Cory';

    //This too is cool
    vm.lastName = 'Brown';

    //Oh noes! logic!!!
    //Yes it's contrived. What do you want from me?
    vm.fullName = function () {

        return vm.firstName + ' ' + vm.lastName;
    };

}

Behold, a contrived bad example.

angular.module('my-app')
    .factory('fullNameinator', fullNameinator);

function fullNameinator() {

    return function (first, last) {

        //Oh yes, this is where logic goes!
        return first + last;
    }
}
angular.module('my-app').controller('myCtrl', MyCtrl);

MyCtrl.$inject = ['firstNameinator'];
function MyCtrl(fn) {
    //cache our reference
    var vm = this;
    
    //This is cool
    vm.firstName = 'Cory';

    //This too is cool
    vm.lastName = 'Brown';

    //Now I'm cool too!
    vm.fullName = fn(vm.firstName, vm.lastName);
}

Services

Services are singletons

.service methods invoke new when they are called.

Since it is invoked with new, properties are attached via this.

Despite being a constructor, only one instance object is created for any given service. That one instance object is returned each time the service is injected.

angular.module('my-app')
    .service('my-service', MyService);


function MyService() {

    //this.now will always be the same time, when it was first instansiated
    this.now = new Date();
}

Factories are singletons

.factory methods do not invoke new when they are called.

Because of this, an object needs to be returned. from the factory.

angular.module('my-app')
    .factory('my-service', MyService);


function MyService() {
    return {
        //This too will always be the same time.
        now = new Date();
    };
}

Use .factory over .service

Like, pretty much always.

references

Angular 1.4 best practices

By Cory Brown

Angular 1.4 best practices

  • 2,192