Search code examples
angularjsangularjs-directiveangularjs-controller

AngularJS: Should I convert directive's linking function to a controller?


I heard it's a good practice to use the controllerAs syntax along with bindToController: true in directives that use an isolate scope. References: one, two

Suppose, I have a directive like this:

angular.module('MyModule').directive('MyDirective', function(User) {
  return {
    scope: {
      name: '='
    },
    templateUrl: 'my-template.html',
    link: function(scope) {
      scope.User = User;
      scope.doSomething = function() {
        // Do something cool
      };
    }
  };
});
<!-- my-template.html -->
<div>
  User Id: {{ User.id }}
  Name: {{ name }}
  <button ng-click="doSomething()">Do it</button>
</div>

As you can see, there is no controller in this directive. But, to be able to leverage controllerAs and bindToController: true I have to have a controller.

Is the best practice to convert the linking function to a controller?

angular.module('MyModule').directive('MyDirective', function(User) {
  return {
    scope: {
      name: '='
    },
    templateUrl: 'my-template.html',
    bindToController: true,
    controllerAs: 'myCtrl',
    controller: function() {
      this.User = User;
      this.doSomething = function() {
        // Do something cool
      };
    }
  };
});
<!-- my-template.html -->
<div>
  User Id: {{ myCtrl.User.id }}
  Name: {{ myCtrl.name }}
  <button ng-click="myCtrl.doSomething()">Do it</button>
</div>

My understanding is that directive's controller should be used as a mechanism to expose directive's API for a directive-to-directive communication.

Could anyone shed light on what's the best practice these days, having Angular 2.0 in mind?


Solution

  • I consider it best practice to move initialization code and/or exposing API functions inside of a directive's controller, because it serves two purposes:

    1. Intialization of $scope 
    2. Exposing an API for communication between directives
    

    Initialization of Scope

    Suppose your directive defines a child scope (or inherits scope). If you initialize scope inside of your link function, then child scopes will not be able to access any scope variables defined here through scope inheritance. This is because the parent link function is always executed after the child link function. For this reason, the proper place for scope initialization is inside of the controller function.

    Exposing a Controller API

    Child directives can access the parent directive's controller through the 'require' property on the directive definition object. This allows directives to communicate. In order for this to work, the parent controller must be fully defined, so that it can be accessed from the child directive's link function. The best place to implement this is in the definition of the controller function itself. Parent controller functions are always called before child controller functions.

    Final Thoughts

    It is important to understand that the link function and the controller function serves two very different purposes. The controller function was designed for initialization and directive communication, and the linker function was designed for run-time behavior. Based on the intent of your code, you should be able to decide whether it belongs in the controller, or it belongs in the linker.

    Should you move any code that initializes scope from the link function to the controller function?

    Yes, that is one of the primary reasons that the controller function exists: to initialize scope, and allow its scope to participate in prototypical scope inheritance.

    Should you move $watch handlers from the link function to the controller function?

    No. The purpose of the link function is to hookup behavior and potentially manipulate the DOM. In the link function, all directives have been compiled, and all child link functions have already executed. This makes it an ideal place to hookup behavior because it is as close DOM ready as it can be (it is not truly DOM ready until after the Render phase).