16
votes

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?

5
Correct me if I'm wrong but isn't the idea of directive itself was for binding the real HTML elements to some behaviours, in order to change the element whenever data changes (reflect the changes back to end users) or update the data when users interact with the element (through DOM events)? If so, the question here will be how to separate the logic out of the directives (let controllers handle that), and let directives handle GUI changes only. Do you think so?Mr. Duc Nguyen

5 Answers

14
votes

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).

3
votes

I will start with your last sentence. It's all about how you want to write your angular code. If you want to stick with the guideline for writing good code for angular 1.x then don't even bother thinking too much about what is ideal. However, if you want to prepare for the next version of Angular, as well as, the upcoming web technologies, I would suggest that you start adopting the new concepts and adjust them to the way you write your code today. Bare in mind there is no right or wrong in this case.

Speaking about angular 2.0 and ES6, I would like to stress out that the notion of directives will be more in align with the Web Components technology.

In Angular 2.0 (according to the current design) will get rid of the complex way of defining directives; That is no more DDO. Thus I think it would be better if you start thinking in that way. A component will just have a View and a controller.

For example,

@ComponentDirective({
    selector:'carousel',
    directives:[NgRepeat]
})
export class Carousel{  
    constructor(panes:Query<CarouselItem>) {
        this.items= panes;
    }

    select(selectedCarouselItem:CarouselItem) { ... }
}

The above code is written in AtScript (a superset of typescript and ES6), but you will be able to express the same thing in ES5, as well. You can see how simpler things will be. There in np such notion like link function or compile etc.

In addition, the view of the above component will be directly bound to the above class; So you can already find a similarity to the controllerAs syntax.

So in essence, I would suggest that you first look at the general idea behind Web Components, and how the future of the Web Developments might be, and then I think you would start writing Angular 1.x code with that in mind.

In summary, try to code in a way that favours the current version of Angular, but if you believe that there are some parts of your code that can embrace some concepts of the next version, then do it. I don't believe it will harm you. Try to keep it simple as the new version of Angular will be simpler.

I would suggest that you read the following posts:

  1. https://www.airpair.com/angularjs/posts/component-based-angularjs-directives
  2. http://eisenbergeffect.bluespire.com/all-about-angular-2-0/
  3. https://www.airpair.com/angularjs/posts/preparing-for-the-future-of-angularjs
  4. http://teropa.info/blog/2014/10/24/how-ive-improved-my-angular-apps-by-banning-ng-controller.html
2
votes

UPDATE

(at the bottom I added a code/plnkr that shows the approach)

Apart from the article you mentioned: https://www.airpair.com/angularjs/posts/preparing-for-the-future-of-angularjs#3-3-match-controllers-with-directives, which basically not only advocates the pattern you are asking for, but component based front-end in general, I have found: http://joelhooks.com/blog/2014/02/11/lets-make-full-ass-angularjs-directives/ (it advocates Minimal use of the link function and use ui-bootstrap as an example where such a pattern has been used). I cannot agree more with both these articles.

Another thing about Angular2.0: no more $scope in angular2.0 -- https://www.youtube.com/watch?v=gNmWybAyBHI&t=12m14s, so surely if you can get rid of $scope as much as possible, then the transition should be smoother.

I made a small mistake as well:

Still, I prefer to define all functions in controller and just call them via link's scope. Ideally it is just one call: scope.init ctrl.init(/*args*/) (where ctrl is directive's controller).


To some degree it is a matter of taste, but there are some valid reasons to keep the link function as thin as possible:

  1. The logic in link function is not easily testable. Sure, you can compile the directive in your unit tests and test its behaviour, but the link function itself is a black box.

  2. If you have to use controller (let say to inter directive communication), then you end up with two places where to put your code. It is confusing, but if you decide to have the link function thin, then everything that can be put in controller should be put in controller.

  3. You cannot inject additional dependencies directly to the link function (you can still use those injected to the main directive function). There is no such a problem in case of controller's approach. Why it matters:

    • it keeps better structure of the code, by having the dependencies closer to the context where they are needed
    • people coming to angular with non-JS backgrounds have still problems how functional closure works in JS

So what has to be put in the link function:

  1. Everything that needs to be run after the element has been inserted into DOM. If $element exposed $on('linked') event than basically this point is not valid.
  2. Grabbing references to controllers require:ed. Again, if it was possible to inject them into the controller directly...

Still, I prefer to define all functions in controller and just call them via link's scope. Ideally it is just one call: scope.init.


Misko Hevery told a couple of times that DDO is far from being perfect and easy to understand and it evolved to what it is right now. I am pretty sure, that if the design decisions were made upfront then there would a single place to put the logic of the directive - as it will be in angular2.0.


Now answering your question if you should convert link function to a controller. It really depends on a number of criteria, but if the code is actively developed then probably it is worth to consider. My experience (and couple of people I talked about it) can be illustrated by this image: The Link Mess Zenith

About angular2.0 -- it is going to be a tectonic shift, so from that perspective it should not matter much, but still the controller's approach seems to be closer to the way directives/components are going to be declared in v2.0 via ES6 classes.

And as the last thing: To some degree it is a matter of taste, but there are some valid reasons to keep the CONTROLLER function thin as well (by delegating logic to services).


UPDATE -- PLNKR

PLNKR exemplifying the approach:

html

<input ng-model="data.name"/>

<top-directive>
  <my-directive my-config="data">

  </my-directive>
</top-directive>

js

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.data = { name : 'Hello, World'};
});

app.controller('MyCtrl', function($scope){

  var self = this;

  this.init = function(top){
    this.topCtrl = top;
    this.getTopName = top.getName.bind(top);
    this.getConfigName = function(){return this.config.name}; 
    console.log('initilizing', this, $scope, this.getConfigName, this.getTopName());
  }

  // if you want to $watch you have to inject $scope
  // you have access to the controller via name defined 
  // in contollerAs
  $scope.$watch('myCtrl.config', function(){
    console.log('config changed', self.getConfigName());
  }, true);
});

app.directive('topDirective', function(){

  return {
    controller : function(){
      this.name = "Hello, Top World";
      this.getName = function(){return this.name};
    }
  }

});

app.directive('myDirective', function(){

  return {

    require: ['myDirective', '^topDirective'],

    controller : 'MyCtrl',
    bindToController: true,
    controllerAs: 'myCtrl',

    template : '{{myCtrl.getConfigName() + " --- " + myCtrl.getTopName()}} ',

    scope : {
     config : "=myConfig",
    },

    link : function(scope, element, attrs,  Ctrls){
      Ctrls[0].init(Ctrls[1]);
    }
  }

});

-1
votes

As per the latest documentation this is still the recommended practice "use controller when you want to expose an API to other directives. Otherwise use link." I would like to hear from other people also and the approach they are using.

-1
votes

sharing the contents from here, (I dont have enough reputations to put it as comments)

“where do I put code, in ‘controller’ or ‘link’?”

  • Before compilation? – Controller
  • After compilation? – Link

Couple of things to note:

  1. controller ‘$scope’ and link ‘scope’ are the same thing. The difference is paramaters sent to the controller get there through Dependency Injection (so calling it ‘$scope’ is required), where parameters sent to link are standard order based funcitons. All of the angular examples will use ‘scope’ when in the context, but I usually call it $scope for sanity reasons: http://plnkr.co/edit/lqcoJj?p=preview

  2. the $scope/scope in this example is simply the one passed in from the parent controller.

  3. ‘link’ in directives are actually the ‘post-link’ function (see rendering pipeline below). Since pre-link is rarely used, the ‘link’ option is just a shortcut to setting up a ‘post-link’ function.

So, whats a real world example? Well, when I’m deciding, I go by this:

  • “Am I just doing template and scope things?” – goes into controller
  • “Am I adding some coolbeans jquery library?” – goes in link

Credit for the answer goes to jasonmore