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

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?

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

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:
https://www.airpair.com/angularjs/posts/component-based-angularjs-directives
http://eisenbergeffect.bluespire.com/all-about-angular-2-0/
https://www.airpair.com/angularjs/posts/preparing-for-the-future-of-angularjs
http://teropa.info/blog/2014/10/24/how-ive-improved-my-angular-apps-by-banning-ng-controller.html

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:
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.
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.
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:
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.
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:
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]);
}
}
});

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.

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:
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
the $scope/scope in this example is simply the one passed in from the parent controller.
‘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

Related

Angular: using templateUrl breaks compile-link ordering in directives

I've came across with problem exposing api of directives in order to its interaction with controller.
There is a simplified jsfiddle to describe my structure.
The problem is that directives has templateUrl property (I replaced it with template in fiddle above), it leads to loading templates async (correct behavior according to this question).
As a result directive's controller is called after main controller therefore directive's api function doWork is undefined (even if you wrap it call with something like $timeout):
.controller('MainCtrl', function($scope, $timeout) {
$scope.api = {};
$scope.init = function() {
$timeout(function() {
$scope.api.doWork();
})
}
$scope.init()
})
There is an approach that comes to my mind - use event in link function of directive and subscribe $scope.api.doWork to that event. But I'm not happy about using events. Also it's not clear how to handle such case if there are some directives nested to each other with similar way of exposing api.
On the other hand it's possible to replace templateUrl with template - but it's also quite bad decision in case of complex layout in template.
Hence, I stuck a bit with the way to resolve such kind of problem. What's the best way to fix it out? Probably there is another technique to expose directive's api (I was inspired by Andrej Kaurin answer in that thread)?
If you are using Angular 1.5+ and are using components you could try $postLink or $onInit function link. Otherwise you just create onDoWork directive scope property and then apply some function from main controller that will be fired when doWork will actually happen(since I think that directive should control when to doWork, if not then maybe you should just create service ?)

AngularJS: Why and when people declare controller inside custom directives

I have seen a couple of custom directives sample code and saw people declare controller inside custom directives. I am new in angular and that is why just do not understand when and why people declare controller inside custom directives?
here one usage
app.directive('someDirective', function () {
return {
scope: {},
controller: function () {
this.name = 'Pascal'
},
controllerAs: 'ctrl',
template: '<div>{{ctrl.name}}</div>'
};
});
Code is taken from http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html
Also like to know what is the usage of bindToController: true,? what it does and when people use it.
Please help me to understand this with a couple of example and code. I need to understand the importance of declaring controller inside custom directives.
thanks
As I understand (and what comes from this answer), you need that if you want to share particular functionality/data among different directives.
Generally, that's done as a correct practice of using separation of concerns. Even though the code controller body lies inside of the directive definition, you still separate data (which controller is in charge of), and DOM-manipulations, which link method is intended to exist for.
If this about why there is controller code at all (and why it's not defined somewhere outside), the I would say "why not?"
If this just a small amount of code which specific particularly for this very directive, it's ok to define it right at a place.
But from my understanding, such cases might be better served with defining filters, but that all depends on particular app architecture.

AngularJS - Sharing data from a parent route controller to all child directives on the page

I have a page that has multiple components and I've created each component as a directive. When the page is first loaded, that's when I grab all the data that should be available on the page. So all of the data exists on the controller for that route, which we'll just call pageCtrl. And then what I've been doing is binding any required data to each directive through the attributes, which of course ends up creating an isolate scope for each of them.
I know there are a few ways to share data, so given this situation, is there a recommended way of doing it? Or has anyone had better success doing it one particular way? While it's working perfectly fine the way I'm doing it, I've run into a few caveats. If I need just even one bit of information from the pageCtrl, I need to add another attribute to the directive. So it ends up creating more code on the directive element itself.
I was thinking about just creating a service that would store all the data, which the pageCtrl could initialize, instead of setting it on itself. Any feedback would be appreciated.
good question :)
First solution is to create in parent controller object and pass this object (via ng-model) to all directives. This object will be passed by reference (not by value) so controller and all directives will have access to the same object.
```
// in controller
$scope.shared_data = {someItems: []};
// in html
<my-directive ng-model=shared_data></my-directive>
Second solution is to create some simple service to store all of those data.
// in this solution you have to inject additional service to directive controller
(extended idea of point 2) creating service/factory that will be responsible by collecting and returning data. This service could be injected into directive and use the same methods to collect data. To avoid making multiple calls to API (REST) it could have some cache for each sensitive method.
Communication via events.... (probably the worsts solution for your example)
The first two ideas are probably the best, I do not know full specification of your product so final solution picking belongs to You:).
My advice is to try/play with all of those methods to really understand what is going on and how and when to use each of them :)
You can directly call your parent controller from child directive controller by using $parent.
App.controller('aCtrl', ['$scope', function ($scope) {
$scope.refresh=function(){
.......... //Updated Data get from DB
};
...........
}]);
App.directive('bDirective',function(){
restrict: 'EC',
replace: true,
scope: {},
controller: function($scope) {
$scope.$parent.refresh();
}
...
});
HTML:
<div ng-controller="aCtrl">
<div class="bDirective"></div> //directive
</div>

Instantiating a controller dynamically from a string and attaching it to an element

I'm writing code that lets me create dialogs that have some shared elements (e.g. cancel button, ok button, title) but you can also embed your own template and controller to customise it more. When you create the dialog, you specify the template and controller you want in "template" and "controller" field of a dialog "object" which is passed on to the primary controller for handling dialogs. The dialog controller now needs to embed the template and instantiate the named controller to control the template elements.
The template code I'm trying to use for this part is this:
<ng-include ng-controller="dialog.controller" src="dialog.template">
If I remove the controller part, the template appears properly. The controller part generates this error:
"Argument 'dialog.controller' is not a function, got string"
How do I instantiate the controller?
Edit: As an example, with the Angular UI modal library you can do this to create a controller:
var modalInstance = $modal.open({
templateUrl: 'dialog_form.html',
controller: 'DialogFormController',
resolve: {
options: function() {
return dialog;
}
}
});
Where the controller field is the name of one of your controllers. How can I copy this functionality to specify my controller with a string instead of a function?
Angular Controllers are functions, and when you specify ng-controller, Angular will call that function and treat the return result of it as the controller object. That's why controller definitions are done as function-type constructors.
But when this happens there's an additional piece of magic - Angular has a controller Provider that maintains a registry of known controllers, for a variety of reasons. (For instance, it knows what injections they need.) You can't just define a global function and hope it gets called.
If you want to do this, see the ngController documentation which describes this option:
From https://docs.angularjs.org/api/ng/directive/ngController:
If the current $controllerProvider is configured to use globals (via
$controllerProvider.allowGlobals()), this may also be the name of a
globally accessible constructor function (not recommended).
You would use something like this if you wanted the functions supplied to you to be in a global variable, although as noted above it's not recommended.
ngController can also take an expression. In that case it will look for dialog to be a scope variable in the parent controller where this is used, so in there you would need something like:
$scope.dialog.controller = function() { /* ... */ };
This second technique is less useful if you want to make a generic library, but there are ways around it. For instance, you might have your developers create a dialog collection in $scope or $rootScope:
$rootScope.myDialogs['dialog1']['controller'] = function() { };
and then use this in your template like:
<ng-include ng-controller="myDialogs.dialog1.controller" src="myDialogs.dialog1.template">
Finally, you could implement your own ngInclude directive that just did both of those things together from a single argument ('dialog1'). AngularJS Directives give you incredible control over the templates and controllers used to run them.

Exposing directive controller to parent controller

I want to expose some of my directive's functionality through its controller (think a public API for this directive).
return {
restrict: 'E',
scope: {},
controller: function($scope) {
this.method1 = ...;
this.method2 = ...;
},
controllerAs: 'dir',
link: function (scope, element, attrs) { ... }
}
Then in my parent controller or template call dir.method1 to get stuff accomplished inside the directive. Any ideas if this is possible as of Angular 1.3?
I'd like to refrain from event passing or even function passing, I have heard this is possible although I have never seen an implementation of this.
It is possible, but your issue isn't to figure out how to get the API out. It's how to get TO it from the parent. You're creating an isolate scope through your use of the 'scope' option. You're also making an element-type directive, so I'm guessing you're doing something like this:
<my-parent>
<my-child></my-child>
</my-parent>
where <my-parent> is the parent directive, and <my-child> is the directive with the API you want to expose.
The real question is what you're trying to achieve here. There is totally a way to do what you're asking. Just because the scope is isolated doesn't mean you can't get to it. You can just iterate through the parent $scope's $$childHead/etc list to find the child whose API you want to access. Anything you define in the child like this:
$scope.myApiFunction = function() {
};
will be visible here. (Things you put into 'this' will not - use the $scope storage bucket instead.)
That means if you only had ONE child you could do something like this from the parent controller:
$scope.$$childHead.myApiFunction();
Simple. Also, very crude. There are lots of problems here: what if you have many children? What if this child with its API ends up one level down? Etc. It's breaking all kinds of OO patterns and it's going to get messy, fast.
Your question is very abstract - it might be good if you updated it with an exact example. Without that, let me guess at your goal. There are two ways to do something "like this" that are encouraged within Angular:
Services. Whenever you say "API", think Service first. A service is a singleton (automatically) so it's tailor-made for creating APIs. And services can use the Factory pattern to return objects of a type, so THOSE are tailor made for doing things like having a manager service handle, say, a buddy list in an IM client, with API methods for creating, removing, and finding buddies.
Items that add "optional" functionality to their parents when they're defined. Let's say we have three possible types of tooltips: tooltips that have a hover effect, those that have a click effect, and those that are triggered by a "walkthrough" system in some order. For this kind of thing, the easy thing to do is just reverse the API, like this:
Parent Controller:
$scope.tooltipHandler = {
showTooltip: function() {},
hideTooltip: function() {}
};
Child Controller:
$scope.$parent.tooltipHandler = {
showTooltip: function() {
// Do some real work
},
hideTooltip: function() {
// Do some real work
},
}
What happens here is if there's no tooltip defined, when the parent runs its walkthrough, nothing happens. If you add the blue tooltip display module, when the parent runs its walkthrough now, it's going to show blue tooltips.
Make sense?
I arrived here looking for a similar response. So far the best that I can figure is to do what Angular does with ngForm.
In the documentation clearly states
If the name attribute is specified, the form controller is published onto the current scope under this name.
This basically makes the form controller accessible from anywhere.
If you have the following DOM
<div ng-controller="MyCtrl as parentCtrl">
<form name="parentCtrl.frmCtrl">
<my-child-directive>
</form>
</div>
You can use require: 'ngForm' in my-child-directive to get access from an inside directive. If you are in the parent controller you can access it trough the frmCtrl variable.
Not sure if this is best practice. In ngForm the name attribute works well, but I don't even know how to call such an attribute for a custom directive.
Thats why I arrived here, I wanted to know if this is "The Angular way" and what types of convetions are on the subject.
Hope it helps!

Resources