Can controllers be completely replaced by the link function in a directive? - angularjs

I was reading through this article:
http://teropa.info/blog/2014/10/24/how-ive-improved-my-angular-apps-by-banning-ng-controller.html
Which proposes that controllers be integrated into directives like this in order to remove the need to ever use ng-controller:
angular.module('contestantEditor', [])
.directive('cContestantEditorForm', function() {
return {
scope: {
contestants: '='
},
templateUrl: 'contestant_editor.html',
replace: true,
controller: 'ContestantEditorFormCtrl',
controllerAs: 'ctrl',
bindToController: true
};
})
.controller('ContestantEditorFormCtrl', function($scope) {
this.contestant = {};
this.save = function() {
this.contestants.push(this.contestant);
this.contestant = {};
};
});
In the comments, however, someone else proposed this solution:
angular.module('contestantEditor', [])
.directive('cContestantEditorForm', function () {
return {
scope: {
contestants: '='
},
templateUrl: 'contestant_editor.html',
replace: true,
link: function (scope) {
scope.contestant = {};
scope.save = function() {
scope.contestants.push(scope.contestant);
scope.contestant = {};
};
}
};
});
It achieves the exact same thing as the version with the controller without ever needing to make a controller. So I'm curious as to what the pros and cons of either method are versus writing angular traditionally with ng-controller, and whether controllers are even necessary by the end of it.
Here is the plunker for the first, and here is the second.

In directives, you should use link function whenever you can. Use controllers only when communication to other directives is needed.
You can find more about this discussion here. Specifically this best practice statement:
Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.

Directives and Controllers are two totally different things.
Direvtives should be used for DOM manipulations.
if you want to know use controller inside DDO or use link function for your logic, then answer will be you should use controller in DDO in that cases when you want provide API and require your directive in other directives and use that API in extended directive
Controllers can't be replaced by directive
Controller should contain your businesses logic and it cant be replaced by directive and shouldn't have DOM manipulations.

Related

In a custom directive, what functionality does `controller` have over `link`?

In trying to get a grasp on creating my own AngularJS directives, I have an example that does everything I need, but realize that in borrowing from various examples, I now can create functionality for the directive's view in both the controller as well as the link.
It seems that I could get rid of the controller all together and just put everything into link, or is there something that I can do with the controller that I can't do with link?
http://jsfiddle.net/edwardtanguay/gxr49h96/6
.directive('itemMenu', function () {
var controller = function ($scope) {
var vm = this;
vm.addItem = function () {
$scope.add();
vm.items.push({
'kind': 'undefined',
'firstName': 'Joe',
'lastName': 'Newton',
'age': Math.floor(Math.random() * 60) + 20
});
};
// DOES THE SAME AS THE FUNCTION DEFINED BELOW IN LINK
// $scope.convertToInternal = function(item) {
// item.internalcode = 'X0000';
// item.kind = 'internal';
// };
};
return {
restrict: 'A',
scope: {
item: '=',
add: '&'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
template: '<div ng-include="getTemplateUrl()"></div>',
link: function (scope, element, attrs) {
scope.getTemplateUrl = function () {
switch (scope.item.kind) {
case 'external':
return 'itemMenuTemplateExternal';
case 'internal':
return 'itemMenuTemplateInternal';
default:
return 'itemMenuTemplateUndefined';
}
};
scope.convertToInternal = function(item) {
item.internalcode = 'X0000';
item.kind = 'internal';
};
},
};
})
You may find a lot of watery rants about controller vs link, most of them contain the information from $compile service documentation.
Answering the question directly,
controllers from other modules/files can be plugged into the directive via Angular DI with controller: 'Controller'
controller can be injected with dependencies while link has fixed arguments list of and gets by with directive's dependencies
controller kicks in before link, so it can prepare the scope for linking or recompile the element on some condition ASAP
controller function has this, its code appearance complies to other OOP-like ES5 code, and the methods can be easily transferred between other code parts, e.g. service service or third-party code
as the result, controllers are suited to be defined as ES2015 or TS classes.
directive's controller can be required by child directive and provides convenient one-way interaction between those two
controller makes use of bindToController: true + controllerAs: 'vm' and implements $scope.vm recipe (particularly useful to fight JS prototypal inheritance) while keeping this syntax
bindToController object value provides attribute bindings for inherited scope: true scope, no more $attr.$observe
bindToController object value provides further granularity for isolated scope. If there are certain attributes that should be bound to the controller and accessed with require, it can be done now
Which code goes to controller and which to link is more delicate question.
It is conventional to use Angular controllers as view models, as in MVVM (hence the controllerAs: 'vm' convention), so if there is a job for controllers (i.e. binding the services to scope values or setting up scope watchers) give it to them, leave the rest to link.
Since $attrs, $element and $transclude local dependencies should be injected into controller explicitly, the one may consider it a sign to skip them ($scope should be injected too for $scope.$ methods, but we will just ignore this fact).
There are some non-religious concerns about the job that should be done by link and not by controller. required controllers aren't available in controller itself, so this kind of directive interaction takes place in link. Since controller launches at earlier compilation phase than link, bound attribute values won't be interpolated yet, so the code that depends on these scope values goes to link. The same applies to other DOM-related code, it goes to link for a reason.
It is mostly the matter of proper code style and not real necessity. As of the code in the example, all scope stuff is controller-friendly, I don't think that link should be there at all.

Dependencies between angular directives

I've created a small app that has 2 directives: One that adds a google map div and initialize it, and the second, which shows layers that contain markers. The directives are independent, but I want them to communicate: The layers directive is independent, yet it needs to use the google maps directive to add markers to the map. I used $broadcast through $rootScope to communicate between the directives.
The directives are defined as follows:
angular.module('googleMapModule', [])
.directive('myGoogleMap', function(){
template: '<div id="map" />',
controller: function($scope){
// some initializations
// Listen for event fired when markers are added
$scope.$on('addMarkersEvent', function(e, data){
// do something
}
}
}
angular.module('layersDirective', [])
.directive('myLayers', function() {
templateUrl: 'someLayersHtml.html',
controller: function($http, $scope, $rootScope){
// Get layers of markers, etc.
// On specific layer click, get markers and:
$rootScope.broadcast('addMarkersEvent', {
data: myMarkers
});
}
});
After this long prologue here are my questions:
How should the connection between the two directives be implemented? Is it correct to use $rootScope and $broadcast or should there be a dependency between the myLayers directive and the myGoogleMap directive?
Furthermore, I've read about when to use controller, link and compile, yet I don't know the right way to use them here. My guess is that myGoogleMap should define its API in a controller and that myLayers should be dependent on myGoogleMap.
The example I wrote here works fine in my application. I'm looking for guidance on how to do it right and to understand what I did wrong here.
Thanks, Rotem
There are some ways for directives to cooperate/communicate
If one is a sibling or child of the other, you can use require. E.g. for this template:
<dir1>
<dir2></dir2>
</dir1>
Use this code:
app.directive('dir1', function() {
return {
...
controller: function() {
// define api here
}
};
});
app.directive('dir2', function() {
return {
...
require: '^dir1',
link: function(scope, elem, attrs, dir1Controller) {
// use dir1 api here
}
};
});
A service, used by both directives to communicate. This is easy and works well if the directives can only be instantiated once per view.
Using $broadcast/$emit on the $rootScope (there is a slight difference: $broadcast will "flood" the scope hierarchy, possibly affecting performance; $emit will only call listeners on the $rootScope, but this means you have to do $rootScope.$on() and then remember to deregister the listener when the current scope is destroyed - which means more code). This approach is good for decoupling components. It may become tricky in debugging, i.e. to find where that event came from (as with all event-based systems).
Other
controller, link and compile
Very short:
Use the controller to define the API of a directive, and preferably to define most its logic. Remember the element, the attributes and any transclude function are available to the controller as $element, $attrs and $transclude respectively. So the controller can, in most cases, replace the link function. Also remember that, unlike the link function, the controller is elligible for dependency injection. (However you can still do dependency injection at the directive level, so, after all, the link function can also access dependencies.)
Use the link function to access required controllers (see case 1 above). Or, if you are feeling lazy, to define the directive's logic. I think the controller is cleaner though.
Use the compile function... rarely :) When you need very special transformations to the template (repetition is the first thing that comes to mind - see ng-repeat) or other mystical stuff. I use directives all the time, about 1% of them needs a compile function.
My guess is that myGoogleMap should define its API in the controller and that myLayers should be dependent on myGoogleMap
The question is how will you communicate this API using events? You probably need only to create an API in a custom event object. The listeners of your event will be using that custom event. If so, the controller does not really need to define an API.
As a bottom line, I am perfectly OK with using events in your case.
Generally communication between directives should be handled via controllers and using the require property on the directive definition object.
If we re-work your first directive we can simply add that method to the controller:
directive('myGoogleMap', function () {
return {
template: '<div id="map" />',
controller: function ($scope) {
var _this = this;
//Part of this directives API
_this.addMarkers = function(arg){
//Do stuff
}
}
};
});
Now we can require this controller in another directive, but one little known features is that you can actually require an array of directives. One of those directives can even be yourself.
All of them will be passed, in order, as an array to your link function:
directive('myLayers', function () {
return {
templateUrl: 'someLayersHtml.html',
controller: function ($http, $scope, $rootScore) {
// Some get layers of markers functionality
},
// Require ourselves
// The '?' makes it optional
require: ['myLayers', '?myGoogleMap'],
link: function(scope, elem, attrs, ctrls){
var myLayersCtrl = ctrls[0];
var myGoogleMapCtrl = ctrls[1];
//something happens
if(myGoogleMapCtrl) {
myGoogleMapCtrl.addMarkers(markers);
}
}
};
});
Now you can communicate explicitly opt-in by using the ? which makes the controller optional.
In order for that to work, you have to define both directives in the same module, i.e.:
var module = angular.module('myModule');
module.directive('myGoogleMap', function(){
template: '<div id="map" />',
controller: function($scope){
// some initializations
// Listen to event for adding markers
$scope.$on('addMarkersEvent', function(e, data){
// do something
}
}
}
module.directive('myLayers', function() {
templateUrl: 'someLayersHtml.html',
controller: function($http, $scope, $rootScore){
// Some get layers of markers functionality
// On specific layer click, get markers and:
$rootScope.broadcast('addMarkersEvent', {
data: myMarkers
});
}
});
Read more here.
EDIT:
Sorry i didn't understand your question, but according to your comment, quoting from the AngularJs Best Practices:
Only use .$broadcast(), .$emit() and .$on() for atomic events
that are relevant globally across the entire app (such as a user
authenticating or the app closing). If you want events specific to
modules, services or widgets you should consider Services, Directive
Controllers, or 3rd Party Libs
$scope.$watch() should replace the need for events
Injecting services and calling methods directly is also
useful for direct communication
Directives are able to directly communicate with each other through directive-controllers
You have already highlight one may for the directives to communicate using rootscope.
Another way directive can communicate if they are defined on the same html hierarchy is by use directive controller function. You have highlighted that too in your question. The way it would be done is (assuming myGoogleMap is defined on parent html), the two directive definitions become:
angular.module('googleMapModule', [])
.directive('myGoogleMap', function () {
template: '<div id="map" />',
controller: function ($scope) {
this.addMarkersEvent = function (data) {}
// some initializations
}
angular.module('layersDirective', [])
.directive('myLayers', function ($http, $rootScope) {
templateUrl: 'someLayersHtml.html',
require: '^myGoogleMap',
link: function (scope, element, attrs, myGoogleMapController) {
$scope.doWork = function () {
myGoogleMapController.addMarkersEvent(data);
}
}
});
Here you use the require property on the child directive. Also instead of using child controller all the functionality in the child directive is now added to link function. This is because the child link function has access to parent directive controller.
Just a side note, add both directives to a single module (Update: Actually you can have the directives in different modules, as long as there are being referenced in the main app module.)

Angular.js- Using ._extend() to extend a base Directive- How to replace Directive name?

I wrote a custom base directive. I will be using this base directive for other directives...for instance, I will write a directive for deleteButton and attach it to baseConfirmation.
I could use just use Angular.js directive inheritance, and have deleteButton require baseConfirmation. But I would like to use underscore's _.extend() because it would be cleaner to have something like
_.extend(deleteButtonDirective, baseConfirmation, templateUrl: '/new/url/template);
But the issue is, since .directive('baseConfirmation' is not a object...not sure how I would replace this name. I would need to replace the directive name to deleteButtonDirective. Is this possible with _.extend()?
angular.module('main.vips').directive('baseConfirmation',
function($modal) {
var confirmModal = function(theScope) {
return $modal.open({
templateUrl: '../confirm_modal.html',
scope: theScope,
backdrop: false
});
}
return {
templateUrl: ,
restrict: "AE",
scope: {
'iconAttribute': "#",
},
controller: function($scope, $element, $attrs) {
var modalInstance;
$scope.commentBox = {};
function modalWork() {
modalInstance.result.then(function() {
//if promise is fullfilled
console.log($scope.commentBox.text);
}, function() {
//if promise is deferred/rejected
console.log("Canceled")
})
.finally(function() {
$scope.commentBox.text = "";
});
}
$scope.cancel = function() {
modalInstance.dismiss();
modalWork();
}
$scope.ok = function() {
modalInstance.close();
modalWork();
}
$element.on("click", function(event) {
modalInstance = confirmModal($scope);
});
}
}
});
This would be incredibly difficult to do - you need to deal with more than just the fact that a Directive isn't a simple object. There is also a cache layer involved that will probably break what you do even if you get it hooked up.
Pawel Gerr recently blogged about an interesting technique for creating 'Dynamic Directives' and he has solutions for these issues in his post, as well as a jsFiddle illustrating his technique. I'm not comfortable doing something like this in my own projects (his technique or yours) because when you start working around frameworks like this and voiding their core philosophies, you'll never know which upcoming release will break your code. I actually think directive inheritance is a pretty good answer, and you won't have a sword hanging over your hand in the future... But if you're determined to do this it's definitely an interesting read! :)
http://weblogs.thinktecture.com/pawel/2014/07/angularjs-dynamic-directives.html

Is it a good practice to have a separate definition for controller when using directive

I am new to angular. And I have been experimenting with "directives". When using directives i found the following 2 ways to use controller with a directive.
Approach 1
angular.module('folderSettingApp')
.directive('templateRenderer', function () {
return {
// other options
, controller: 'GridController'
};
});
Approach 2
angular.module('folderSettingApp')
.directive('templateRenderer', function () {
return {
// other options
, controller: function ($scope, $attrs) {
// add some functions here
}
};
});
I am not sure what approach to use when, any suggestion would be appreciated.
It's not so general to use which one, it's based on your use,
For example, if you want to use a directive multiple time in your app, it's better you don't define any controller in your directive, like this:
.directive('exampleDirective', function (){
return {
restrict: "A", // OR E OR AE OR C
template: "<div>YOUR TEMPLATE HERE</div>",
link: function (scope, element, attributes) {
// what ever function you want whould be here:
}
}
});
But generally, for the sake of angular's modularity, it's not a good idea to define a controller in directives.
Because the aim of directive is to use it in multiple controller and multiple views.
So defining a controller in a directive, just makes that directive less modular with restricted use.
One argument for the separate controller is for ease of testing. It's not hard at all to write tests which instantiate and allow you to test individual code on a separate controller.

Do I need separate controllers from component-like directives in angularjs

I am writing custom element directives which are used to encapsulate HTML GUI or UI components. I am adding custom methods (that handles ng-click events, etc) in my link function such as:
app.directive('addresseseditor', function () {
return {
restrict: "E",
scope: {
addresses: "="
}, // isolated scope
templateUrl: "addresseseditor.html",
link: function(scope, element, attrs) {
scope.addAddress= function() {
scope.addresses.push({ "postCode": "1999" });
}
scope.removeAddress = function (index) {
scope.addresses.splice(index, 1);
}
}
}
});
Is the link function correct place to define the methods or is it better to create a separate controller object, use ng-controller and define methods there?
You can also define a controller per directive if you want. The main difference is that directives can share controllers (at the same level), controllers execute prior to compile, and controllers are injected (hence using the $). I think this is an accepted practice.
app.directive('addresseseditor', function () {
return {
restrict: "E",
scope: {
addresses: "="
}, // isolated scope
templateUrl: "addresseseditor.html",
controller: function($scope, $element, $attrs) {
$scope.addAddress= function() {
$scope.addresses.push({ "postCode": "1999" });
}
$scope.removeAddress = function (index) {
$scope.addresses.splice(index, 1);
}
}
}
});
You can have both link and controller... but you want to do any DOM stuff in the link because you know you're compiled.
This method also remains de-coupled since it's still part of your directive.
If you are permanently coupling this with a controller & view, then I would say it doesn't really matter where you put it.
However, if you one day want to decouple the directive so you can reuse it, think of what functionality needs to be included.
The Angular guide on directives reads:
The link function is responsible for registering DOM listeners as well
as updating the DOM. (...) This is where most of the directive logic
will be put.
I would follow the last part of that statement only if I had to write a directive that heavily manipulates the DOM. When all my directive does is render a template with some functionality, I use the link function to perform whatever basic DOM manipulation I need and the controller function to encapsulate the directive's logic. That way I keep things clearly separate (DOM manipulation from scope manipulation) and it seems consistent with the idea of "view-controller".
FWIW, I've implemented my first open source directive with those things in mind and the source code can be found here. Hopefully it might help you somehow.
If you want your elements functionality 'instance' specific place it in the link function, if you want to create an API across directives on an element create a controller function for it like master Rowny suggests.

Resources