I created a directive, which binds to $stateChangeSuccess events in link function:
module.exports = angular.module("titlePanel", [])
.directive("titlePanel", titlePanel);
function titlePanel($state, $rootScope, $parse, $interpolate) {
return {
restrict: "E",
replace: false,
scope: true,
templateUrl: template,
link: function(scope, element, attrs) {
// some actions
// ...
$rootScope.$on("$stateChangeSuccess", function(){
console.log("State change success happened on titlePanel");
});
}
}
This directive is used only at some pages of my site. What surprises me is that when I transition from state that contains this directive's instance to a state that doesn't contain it anymore, directive still responds to $stateChangeSuccess event and executes console.log().
I believe, I misunderstand the chronological order of events upon transferring to a certain state (I use ui-router). I thought it is as follows:
ui-router starts switching to a new state
it resolves and loads dependencies of new state's controller
it instantiates new controller
it traverses through the code of new controller's template and detects directives within it
it compiles those directives if they were not already compiled and stored in module registry
it creates directive instances by creating directive scopes and calling link linking function
it destroys the previous state, previous controller/scope and instances of directives, built for the previous controller
it emits $stateChangeSuccess
I am probably wrong somewhere. Can you describe the correct order of events?
Directives are destroyed once they are no longer represented in the DOM. (They do persist if they're not visible, hence ng-show=false will hide but not destroy the directive; ng-if=false will destroy it.)
But:
$rootScope.$on("$stateChangeSuccess", function(){
console.log("State change success happened on titlePanel");
});
This attaches the event to rootScope, which persists throughout the lifespan of the application, even after the directive is gone.
You can prevent this by either explicitly detaching the event on the directive's 'destroy' method:
var unbinder = $rootScope.$on("$stateChangeSuccess", function() {
console.log("State change success happened on titlePanel");
});
scope.$on('$destroy', function() {
unbinder();
});
...or, better, by attaching the event to the directive's scope rather than the root scope, so it will be cleaned up automatically. (It's generally good practice to avoid using root scope when not absolutely necessary: instead bind events to the directive they belong to, or to a shared factory or service devoted to that purpose.)
Related
I am looking to find the best way of sending scope through nested directives.
I have found that you can do $scope.$parent.value, but I understood that's not a best practice and should be avoided.
So my question is, if I have 4 nested directives like below, each with it's own controller where some data is being modified, what's the best way to access a value from directive4 (let's say $scope.valueFromDirective4) in directive1?
<directive1>
<directive2>
<directive3>
<directive4>
</directive4>
</directive3>
</directive2>
</directive1>
For the "presentational" / "dumb" components (directive3 and directive4), I think they should each take in a callback function which they can invoke with new data when they change:
scope: {
// Invoke this with new data
onChange: '&',
// Optional if you want to bind the data yourself and then call `onChange`
data: '='
}
Just pass the callback down from directive2 through directive4. This way directive3 and directive4 are decoupled from your app and reusable.
If they are form-like directives (similar to input etc), another option is to look into having them require ngModel and have them use ngModelController to update the parent and view. (Look up $render and $setViewValue for more info on this). This way you can use them like:
<directive4 ng-model="someObj.someProp" ng-change="someFunc()"></directive4>
When you do it like this, after the model is updated the ng-change function is automatically invoked.
For the "container" / "smart" directives (directive1 and directive2), you could also have directive2 take in the callback which is passed in from directive1. But since directive1 and directive2 can both know about your app, you could write a service which is injected and shared between directive1 and directive2.
Nested directives can always have an access to their parents' controllers via require. Let's say you want to change value from the directive1's scope from any of its nested directives. One of the possible ways to achieve that is to declare a setter in the directive1's controller setValue(value). Then in any of nested directives you need to require the directive1's controller and by doing that you'll get an access to the setter setValue(value) and other methods the controller provides.
angular
.module('yourModule')
.directive('directive1', function() {
return {
controller:['$scope', funciton($scope) {
return {
setValue: setValue
};
funciton setValue(value) {
$scope.value = value;
}
}]
// The rest of the directive1's configuration
};
})
.directive('directive4', function() {
return {
require: '^^directive1',
link: (scope, elem, attrs, directive1Ctrl) {
// Here you can call directive1Ctrl.setValue() directly
}
// The rest of the directive4's configuration
};
})
Another way is to $emit events from a child directive's controller whenever value is changed by the child. In this case the parent directive's controller should subscribe to that event and handle the data passed along with it.
I have created a directive (ParentDir) that has it's own controller and whose template binds to this controller. It communicates with another directive (Child1) that has it's own controller which 'requires' the first parent directive. Below is a simplified example:
Module.directive("ParentDir", function () {
return {
templateUrl: '../ParentTemplate',
restrict: 'AEC',
scope: {
},
controllerAs: 'parCtrl',
bindToController: true,
controller: ['$scope', function ($scope) {
parCtrl= this;
parCtrl.title = "PARENT 1 TITLE";
}]}
Module.directive("Child1", function () {
return {
templateUrl: '../Child1Template',
restrict: 'AEC',
require: '^^ParentDir',
scope: {},
controllerAs: 'ch1Ctrl',
bindToController: true,
link: function ($scope, element, attrs, parCtrl) {
$scope.parCtrl= parCtrl;
},
controller: ['$scope', function ($scope) {
ch1Ctrl= this;
ch1Ctrl.title = "CHILD 1 TITLE";
}]}
ParentDir html:
<child1> </child1>
Child1 html:
{{parCtrl.title}}
{{ch1Ctrl.title}}
Finally my ParentDirective is initialized in something like this:
<div ng-animate-swap="trigger" class="swapclass">
<parent-dir></parent-dir>
</div>
I need the entire parent directive's template to slide in certain cases. I also use the directive in other places where I don't need this and I can use it as is. In the cases where I do need the slide animation, I place it inside an ng-animate-swap as shown above. The problem is that every time the swap trigger changes, a new parCtrl is initialized causing everything to be reset!
How can I use animate swap with a directive that has isolate scope and it's own controller, without reinitializing the controller everytime a swap occurs?
Directives, as we know, are high-level markers that tell Angular's compiler to attach a specified behavior to that HTML element. When a directive is put on the DOM, Angular's $compile service matches directive names with their code, normalizes it and executes it.
However, ng-animate-swap removes its element from the DOM before appending the new one.
This means your directives are being recompiled for each swap, and new isolate scopes are created each time the animation happens.
The solution to this depends on the functionality of your app, how large your templates are, and how often you need to do the animation (or what the animation entails):
One solution is to create another directive outside of the swap animation that holds parCtrl.title and ch1Ctrl.title (or whatever other variables you have) and then is able to pass that information down to child scopes through prototypical inheritance:
<swap-dir>
<div ng-animate-swap="trigger" class="swapclass">
<parent-dir></parent-dir>
</div>
<swap-dir>
This could also be done with a controller, perhaps much more easily. What you choose to do would depend on where you're getting your scope variables from and how many different elements you have on the page.
However, ng-animate-swap creates its own scope, so while I believe this would work, the ever-fun JavaScript inheritance shenanigans could cause an issue here as well.
Another solution would be to skip ng-animate-swap altogether and just animate the template element with regular old CSS transitions, although this depends on what you're doing and how you want it to look.
Consider
angular.module('App').directive('errors',function() {
return {
restrict: 'A',
controller:function() {
var self = this;
self.closeErrors = function() {
self.errors = [];
self.hasErrors = false;
}
},
controllerAs: 'errorsCtrl',
templateUrl: 'errors.html'
}
when called with
<div errors="otherCtrl.errors"></div>
the object errors comes from another controller.
I know i can add
scope: {errors:"="},
and then access it in my controller via
$scope.errors;
but when I assign it to
self.errors = $scope.errors.
self.errors never gets updated when it is changed in the parent.
So my question is, how can I let this work that whenerver my parentcontroller changes the errors object it is also changed in the errorsCtrl.
(Also I do know I can access errors directly in my template without the controller, but I simply want to use my errorsCtrl)
Add bindToController: true to your directive.
http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html
Angular 1.3 introduces a new property to the directive definition
object called bindToController, which does exactly what it says. When
set to true in a directive with isolated scope that uses controllerAs,
the component’s properties are bound to the controller rather than to
the scope.
That means, Angular makes sure that, when the controller is
instantiated, the initial values of the isolated scope bindings are
available on this, and future changes are also automatically
available.
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.)
I have a directive for a chart:
.directive('chart', function() {
return {
...
controller: function($scope) {
this.toggleAnimation = function() {
...
};
},
link: function link(scope, element, attrs) {
...
}
}
});
And I'm using it like so:
<div ng-controller='foo'>
<chart></chart>
</div>
Where foo is:
.controller('foo', function($scope) {
// TODO: call chart's toggleAnimation
});
Now, how do I call the toggleAnimation function on the chart directive from within foo controller?
Or is this not how the setup should be? What I'm trying to do here is create a function for my chart directive that allows whatever's consuming it to turn a variable in the directive to true/false.
.directive('chart', function() {
return {
scope: {
toggle: "#" // pass as string - one way // you could also make this an attr if you want
},
...
controller: function($scope) {
},
link: function link(scope, element, attrs) {
...
var toggleAnimation = function() {
...
};
// when you change this value, it will toggle the animation
// logic which will check the values of this variable so you can do if statements and modify animations
scope.$watch('toggle', function(newVal, oldVal){
console.log(newVal);
if(parseInt(newVal) === 1)
toggleAnimation();
else if(parseInt(newVal) === 0)
; // do something else like toggle back
});
}
}
});
HTML
<div ng-controller='foo'>
<chart toggle="myVariable"></chart>
</div>
Controller JS
.controller('foo', function($scope) {
// TODO: call chart's toggleAnimation
$scope.myVariable = 0; // initialize to this value
function clickSomething(){
$scope.myVariable = 1; // change, hence fire animation
}
});
There are two main mechanisms by which data can flow between specific directives or controllers. Data can either flow down the scope hierarchy (which usually mirrors the DOM tree) using scopes and expressions, or it can flow up the hierarchy using directive controller APIs. Both of these mechanisms entail one directive communicating with one other specific directive.
A third communication mechanism is scope events. This mechanism is about one directive communicating with zero or more other directives/controllers which it doesn't necessarily know about.
Which mechanism to use depends on the specific scenario. The following sections give an overview of each, followed by a round-up of the trade-offs of each. (In the specific example you gave I'd use the first, but you seem to be interested in the general mechanisms and not just in your specific example.)
The idiomatic way to pass data down the tree is to provide the chart access to data from its parent scope. In that case, it would be used like this:
<div ng-controller="Foo">
<chart animated="chartAnimated"></chart>
</div>
The chartAnimated in the above is a scope variable inserted by the controller. Here's how that looks in the Foo controller:
.controller('Foo', function($scope) {
$scope.chartAnimated = true;
$scope.toggleAnimation = function () {
$scope.chartAnimated = ! $scope.chartAnimated;
};
});
The chart directive then needs to support this new attribute, which can be achieved using the scope property in the directive declaration:
.directive('chart', function() {
return {
scope: {
// This requests that Angular parse the expression in the 'animated'
// attribute and write a function for it into the scope as
// 'animationEnabled'.
'animationEnabled': '&animated'
},
link: function link(scope, iElement, attrs) {
// Now we can watch the expression to detect when it changes.
scope.$watch(
scope.animationEnabled,
function (isEnabled) {
// This function will be called once on instantiation and then
// again each time the value of the expression changes.
// Use ``isEnabled`` in here to either enable or disable animation.
console.log('Animation', isEnabled ? 'is enabled' : 'is disabled');
}
);
}
}
});
Although it's not really applicable to your given example, let's also explore the other data flow technique I mentioned, where data flows up the tree.
In this case, a parent directive can expose an API to a child directive. This is, for example, how the ngModel directive interacts with its parent form directive, or how ngSwitchWheninteracts with its parent ngSwitch.
The key here is the require property on the directive declaration, which allows a directive to depend on another directive either on the current element or on some parent element. For the sake of this example, we'll look for it on any parent element.
Let's make a contrived example of a parent directive with many children that it wants to keep track of for some reason:
<parent>
<child name="foo"></child>
<child name="bar"></child>
<child name="baz"></child>
</parent>
We'll define the parent directive first:
.directive('parent', function() {
return {
controller: function () {
this.children = {};
this.registerChild(name, child) {
console.log('Got registration for child', name);
this.children[name] = child;
}
}
}
});
The child directive is where we can make use of the require mechanism:
.directive('child', function() {
return {
require: '^parent', // Must be nested inside a 'parent' directive
link: function (scope, iElement, attrs, parentCtrl) {
// Notice the extra 'parentCtrl' parameter above.
// Provide an API for parent to interact with child.
var child = {};
child.doSomething = function () {
console.log('Child', attrs.name, 'requested to do something');
};
parentCtrl.registerChild(attrs.name, child);
}
}
});
In this case we establish a bidirectional communication channel between the parent and the child, with the child initiating the channel using require, and passing to the parent an object through which it can communicate with the child. When require is used there is an extra argument to link giving the controller of the directive that was requested.
Finally, let's talk about events. These are best applied in a situation where you have a single directive (or indeed, any other code that owns a scope) that wishes to broadcast a particular notification to whoever is listening. For example, $route communicates with ng-view (and anyone else who is listening) using the $routeChangeSuccess event, and ng-view alerts the rest of the application that the view is ready via $viewContentLoaded.
Event watchers belong to scopes, and events propagate up and down the scope hierarchy.
If you're holding a scope, you can watch for any events that might pass by using scope.$on:
scope.$on(
'$viewContentLoaded',
function () {
console.log('view content loaded!');
}
);
If you want to send an event, you can either send a message up the scope hierarchy using $emit:
scope.$emit(
'somethingHappened'
);
...or you can send a message down the scope hierarchy using $broadcast:
scope.$broadcast(
'somethingHappened'
);
In some circumstances you wish to pass an event to the entire application, in which case you can $broadcast on the $rootScope:
$rootScope.$broadcast(
'somethingHappened'
);
One important thing to keep in mind with events is that they are a "point-to-multipoint" mechanism, which is to say that many different recipients may "see" the same message. This makes events a rather poor mechanism for directed communication between two specific participants, and so events should be used sparingly.
So there's an overview of three data-flow mechanisms for directives in AngularJS. There are different trade-offs for each:
Passing data to child directives via scope keeps the child directive decoupled from the parent, but requires the parent directive (or, in your case, controller) to provide the data that the child needs.
The require mechanism is best used to provide template constructs that require the participation of multiple strongly-related elements, such as in the case of ngSwitch where ngSwitchWhen exists only to be used with ngSwitch, and is of no use without it.
Events are best used for broad notifications, where the sender doesn't especially care who receives the message, and the recipient doesn't necessarily know who sent it. In this case, sender and recipient are truly decoupled from one another, and there may not even be a recipient.
You can maybe use $scope.$broadcast to broadcast an event to the child scope in the directive. The directive would listen for that and then run the method when it hears the correct event:
$scope.$broadcast("toggleAnimation", this.textToBroadcast);
Fiddle here:
http://jsfiddle.net/smaye81/q2hbnL5b/3/