When going through egghead video, the one about Directive to directive communication suggests we use controller to add functions to 'this' object and access it from other directives.
The full code used in the video: Adding functions to this object
The relevant controller code is as follows:
controller: function($scope){
$scope.abilities = [];
this.addStrength = function(){
$scope.abilities.push("Strength");
}
this.addSpeed = function(){
$scope.abilities.push("Speed");
}
this.addFlight = function(){
$scope.abilities.push("Flight");
}
},
I was wondering instead of adding functions to 'this' why not add it to the $scope itself especially when we are using isolated scope?
Code adding functions to $scope: Adding functions to $scope
The relevant controller code is as follows:
controller: function($scope){
$scope.abilities = [];
$scope.addStrength = function(){
$scope.abilities.push("Strength");
};
$scope.addSpeed = function(){
$scope.abilities.push("Speed");
};
$scope.addFlight = function(){
$scope.abilities.push("Flight");
};
},
Or why have the controller function at all. Why can not we use the link function to achieve the same result?
Adding functions to $scope in the link function: Using link funtciont instead of controller
The relevant controller and link function is as follows:
controller: function($scope){
$scope.abilities = [];
$scope.addStrength = function(){
$scope.abilities.push("Strength");
};
$scope.addSpeed = function(){
$scope.abilities.push("Speed");
};
$scope.addFlight = function(){
$scope.abilities.push("Flight");
};
},
I am pretty sure there is valid reason to use controller and this object. I am not able to understand why.
You are correct that you can expose functions in the link function and get the same results. Directive controllers are a bit of an odd bird, but as I've written more complex directives, I've settled on pushing as much behavior into controllers and leaving DOM-related stuff in the link function. The reason why is:
I can pass in a controller name instead of having the function inside my directive; things get cleaner IMO
The controllers can expose public APIs used inside of sibling or related directives so you can have some interop and encourage SoC.
You can isolate the controller testing apart from the directive compilation if you like.
I typically only introduce controllers when there are perhaps complex state transitions, external resources being handled (ie $http), or if reuse is a concern.
You should note that Angular 1.2 exposes 'controllerAs' on directives which allows you to directly consume the controller in directive templates and reduce a bit of the ceremony the $scope composition introduces.
Related
just reading a write up from this link http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-6-using-controllers
it was hard me like novice in ng to understand their code sample. just tell me a example where people would write directive without controller ?
their code
(function() {
var app = angular.module('directivesModule');
app.directive('isolateScopeWithController', function () {
var controller = ['$scope', function ($scope) {
function init() {
$scope.items = angular.copy($scope.datasource);
}
init();
$scope.addItem = function () {
$scope.add();
//Add new customer to directive scope
$scope.items.push({
name: 'New Directive Controller Item'
});
};
}],
template = '<button ng-click="addItem()">Add Item</button><ul>' +
'<li ng-repeat="item in items">{{ ::item.name }}</li></ul>';
return {
restrict: 'EA', //Default in 1.3+
scope: {
datasource: '=',
add: '&',
},
controller: controller,
template: template
};
});
}());
Directive usage:
Attribute: <div isolate-scope-with-controller datasource="customers" add="addCustomer()"></div>
Element: <isolate-scope-with-controller datasource="customers" add="addCustomer()"></isolate-scope-with-controller>
How we can pass customer data directly to directive. basically we have model in controller and populate model and then pass that model data to directive via isolated scope or directive use controller scope. I am very confused the how above code can work, please help me to understand. thanks
The scenario that is being considered implies that the directive will be used in a part of the application, that already has a declared controller, the scope of which contains the properties datasource and add. In turn, new controllers will be instantiated for each of the instances of the directive and will have their own isolate scope.
In reality, it is much more common to create directives that do not have a controller, but rather use the link function. These directives can either rely on the parent controller, sometimes perform DOM manipulation, bind to JS events or simply serve as means to encapsulate part of your application.
You can find a good example of a directive that does not create its own controller here. It is taken from the Angular docs. You will find that it does not even belong to a parent scope in this case meaning that no controller is involved. In reality, an element like this would most probably report to a parent controller, which would then do something with the position.
You can read more about directives, the link function, and how directives work with controllers here.
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.)
So, my impression is that Angularjs can be a bit verbose and I would like some clear understanding of what the main differences are between the following functionality AND when are they used. I 'feel' I understand, but lines are a bit vague & ethereal so I want others to clarify.
What is the difference in usage between: They seem interchangeable in their duties, and usage.
A. Service
B. Factory
C. Provider
What is the difference between a module.controller vs. module.directive.controller?
ie:
var app = angular.module("someApp",[]);
app.controller("someCtrl1", function(){
// some code
});
and this usage, when it sits inside a directive
app.directive("someDirective", function() {
return {
restrict: 'E',
template: '<p>Hello {{name}}!</p>',
controller: function($scope, $element){
$scope.name = $scope.name + "post1 ";
},
link: function(scope, el, attr) {
scope.name = scope.name + "post2 ";
}
}
})
I am a little hazy on the "controller" inside the return of the directive. What is this convention, in more detail?
In a brief, Service is a wrapper for factory and factory is a wrapper for provider.
the service uses a JS constructor syntax while factory uses a JS function syntax.
BUT, there is one big difference between service/factory and a provider, that the provider is accessible during the config phase while the previous are not.
See this good and detailed answer.
The main purpose of a controller in a directive, is when your directive has a isolated scope, so it isn't coupled to the parent controller and therefore it has a independent controller.
1.
You can find a clear answer on this question
Additionaly I can say:
Services : has simple structure
Factories has complicated structure
Providers has more complicated structure
There is no differance between module.controller vs. module.directive.controller.
I did't hear about usage module.directive.controller, Correct usage is;
app.directive("myDir", ['$yourService',function($yourService){
return {
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
var that = this;
//add sume property
that = angular.extend(that, $yourService));
return that;
}]
}
}])
Basicaly, you don't need controller property for each directives. However, if your directive is a terminal directive or a parent directive, and your child directive need some functions or properties of parent directive, you need a custom contoroller in directive.
For example, if you have table directive which have child row and cell directive, table must hava a controller.
So I'm trying the Tabs directive and having some problems.
the structure is something like:
//routes
$routeProvider..when('/course/:id', {
controller: 'CourseCtrl',
templateUrl: '/app/views/course.html'
});
//course.html
<div ng-controller="CourseTabsCtrl">
<tabset>
<tab>
<tab-heading>Title</tab-heading>
<div ng-include="'/view.html'"></div>
</tab>
....
</tabset>
</div>
Problem is i can't access the api to enable or disable tabs, select a tab, in none of the controllers CourseTabsCtrl or CourseCtrl.
Is this because the directive is working on an isolated scope? and if so, is there a way to get around that? How can i fix it?
Thanks
Looking at the source and the documentation, you should be able to pass in an expression to the <tab> directive, that dictates whether it is enabled or disabled.
app.controller('CourseTabsCtrl', function ($scope) {
$scope.expr = true;
});
<div ng-controller="CourseTabsCtrl">
<tabset>
<tab disabled="expr">
<tab-heading>Title</tab-heading>
<div ng-include="'/view.html'"></div>
</tab>
....
</tabset>
</div>
If however, this is not enough for you. You could 'hack' the tabset directive and use another controller than the one currently specified. Now, you would have to replicate the old behaviour of the default TabSetController, but you could add functionality on top of it to cater to your needs.
The best way (I've found) to do this is to decorate the directive itself.
Like so:
app.config(function ($provide) {
$provide.decorator('tabSetDirective', function ($delegate) {
// $delegate in a directive decorator returns an array. The first index is the directive itself.
var dir = $delegate[0];
dir.controller = 'CourseTabsController';
return $delegate;
});
});
You could build further on this and pass in the controller to the directive itself.
<tabset ctrl="someCustomCtrl"></tabset>
The config block would then look like this:
app.config(function ($provide) {
$provide.decorator('tabSetDirective', function ($delegate) {
// $delegate in a directive decorator returns an array. The first index is the directive itself.
var dir = $delegate[0];
dir.controller = function ($scope, $element, $attrs, $controller) {
return $controller($attrs.ctrl, {
$scope: $scope
});
};
return $delegate;
});
});
Note: If you go with the decorator way, you may have to do it in the config block for the angular-ui module. In that case, you will probably want to have a look here, to be able to configure third party modules without touching their core code.
A second gotcha to the passed-in controller way, is that you need to make use of $injector in order to get dependencies (apart from $scope, $element and $attrs) into the controller. This can either be done in the config block, or in the controller itself by adding $injector as a dependency, like so:
app.controller('CourseTabsController', function ($scope, $injector) {
var $timeout = $injector.get('$timeout');
// etc...
});
What the f* am I on about?
Given that I don't personally work with the AngularUI Bootstrap components, and the fact that there is no plunker/jsBin available I'm throwing out some tips n tricks on how to add custom behaviour to third party components, without polluting their core code.
To address the questions at the end of your post:
Is this because the directive is working on an isolated scope?
It very well might be, the idea of isolated scopes is to not pollute the outside world with their inner properties. As such, it's highly likely that the only live 'endpoint' connected to the <tab> directive API is the default AngularUI TabSetController.
... and if so, is there a way to get around that? How can i fix it?
You can either do what I've suggested and roll your own controller (bare in mind that you should duplicate the code from the TabSetController first), that way you should have full access to the endpoint of the <tab> directive. Or, work with the options that are available to the directive as of this writing and wait for some more functionality to be introduced.
I'll try to fire up a jsBin soon enough to further illustrate what I mean.
Edit: We can do the whole decorator dance and duplication of the old controller behaviour, without the need to pass in a new controller. This is how we would achieve that:
app.config(function ($provide) {
$provide.decorator('tabSetDirective', function ($delegate, $controller) {
// $delegate in a directive decorator returns an array. The first index is the directive itself.
var dir = $delegate[0];
var origController = dir.controller;
dir.controller = function ($scope) {
var ctrl = $controller(origController, { $scope: $scope });
$scope.someNewCustomFunction = function () {
console.log('I\'m a new function on the ' + origController);
};
$scope.someNewCustomFunction();
return ctrl;
};
return $delegate;
});
});
Here's a jsBin illustrating the last example: http://jsbin.com/hayulore/1/edit
In my rootscope I have a visible property which controls the visibility of a div
app.run(function ($rootScope) {
$rootScope.visible = false;
});
Example HTML:
<section ng-controller='oneCtrl'>
<button ng-click='toggle()'>toggle</button>
<div ng-show='visible'>
<button ng-click='toggle()'>×</button>
</div>
</section>
Controller:
var oneCtrl = function($scope){
$scope.toggle = function () {
$scope.visible = !$scope.visible;
};
}
The above section works fine, the element is shown or hide without problems. Now in the same page in a different section I try to change the visible variable to show the div but it doesn't work.
<section ng-controller='otherCtrl'>
<button ng-click='showDiv()'>show</button>
</section>
Controller:
var otherCtrl = function($scope){
$scope.showDiv = function () {
$scope.visible = true;
};
}
In AngularJS, $scopes prototypically inherit from their parent scope, all the way up to $rootScope. In JavaScript, primitive types are overwritten when a child changes them. So when you set $scope.visible in one of your controllers, the property on $rootScope was never touched, but rather a new visible property was added to the current scope.
In AngularJS, model values on the scope should always "have a dot", meaning be objects instead of primitives.
However, you can also solve your case by injecting $rootScope:
var otherCtrl = function($scope, $rootScope){
$scope.showDiv = function () {
$rootScope.visible = true;
};
}
How familiar are you with the concept of $scope? It looks to me based on your code that you're maintaining two separate $scope variables called "visible" in two different scopes. Do each of your controllers have their own scopes? This is often the case, in which case you're actually editing different variables both named "visible" when you do a $scope.visible = true in different controllers.
If the visible is truly in the rootscope you can do $rootScope.visible instead of $scope.visible, but this is kind of messy.
One option is to have that "otherCtrl" code section in a directive (you should probably be doing this anyway), and then two-way-bind the directive scope to the parent scope, which you can read up on here. That way both the directive and the page controller are using the same scope object.
In order to better debug your $scope, try the Chrome plugin for Angular, called Batarang. This let's you actually traverse ALL of your scopes and see the Model laid out for you, rather than just hoping you're looking in the right place.