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/
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'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.)
Question in one sentence:
How do I, in a directive, access the controller of a child directive?
Longer description:
I'm writing a couple of directives to handle input from a remote controller (think TV controller). I am doing this because HTML does not have inherently good focus/cursor handling. My problem is that I am new to AngularJS and it feels like it is working against me.
In a particular view for example I want to be able to do something like this:
<div>
<my-linear-focus-container direction="horizontal">
<my-grid default-focused="true" style="...">{{gridItems}}</my-grid>
<my-button style="..."></my-button>
</my-linear-focus-container>
</div>
All views and "widgets" that wants to handle keys needs to have a FocusNode directive. The nodes will together create a focus tree and keys will propagate from the focused node in the tree down the branch to the root. When a new node is focused proper signaling will occur among the relevant tree nodes (lost focus, received focus, etc).
The linearFocusContainer's responsibility will be to switch focus between child widgets/directives. So if child A has focus (and does not listen to the right key) and the user presses right the linearFocusContainer will give focus to child B which lies right next to child A.
LinearFocusContainer directive
{
"restrict": "E",
"scope": {},
"template": "<div rs-focus-node keys='keys'></div>",
"link": function (scope) {
$scope.keyListeners = {
left: function () { /* focus child left of current focused */ },
right: function () { /* focus child right of current focused */ },
...
}
$scope.focusEventListeners = {
onFocusReceived: function () { /* focus to default child */ },
...
}
}
}
Heres my problem. For this to work I need access to the FocusNode directive "owned by" the LinearFocusContainer directive inorder to focus/access other children.
$scope.keyListeners = {
left: function () { focusNode.getChildren()[0].takeFocus(); }
}
That would also give me possibility to do:
focusNode.setKeyListeners({
...
});
And such instead of writing to a variable in the scope.
You cannot access the child controller directly, Angular does not support this.
What you can do is have the child require the parent and pass the child controller to the parent as:
app.directive('parent', function() {
return {
...
controller: function() {
var childController;
this.setChildController = function(c) {
childController = c;
};
}
};
});
app.directive('child', function() {
return {
...
require: ['parent', 'child'],
controller: function() {
...
},
link: function(scope, elem, attrs, ctrls) {
ctrls[0].setChildController(ctrls[1]);
}
};
});
This demonstrates the principle, you can adjust it accordingly if there are more than one children.
Addressing the comment [...] the child does not know what directive the parent is. [...] Can require take "generic types"?
So no, require cannot take generic types (and this would be a useful functionality, I've been running on it a lot lately). I can suggest 2 solutions:
Introduce an extra "coordinator directive". E.g for the case described in the comment, assume the following HTML:
<linear-focus-container focus-coordinator>
<my-grid default-focused="true" style="...">{{gridItems}}</my-grid>
</linear-focus-container>
Both the LinearFocusContainer directive and the myGrid will require the focusCoordinator and cooperate through it. It could even implement some useful common functionality among the different possible types of parent directives.
(highly untested and probably DANGEROUS) All the parent directives put an object with a standard API to the DOM via angular.element.data() under a well defined name. The child directives walk up the hierarchy of their DOM parents looking for this well defined name. At the very least do not forget to remove this object on $destroy.
Why don't you use event broadcast/emit mechanism provided by Angular JS.
$scope.$broadcast will broadcast a event down all the child scopes. You can catch in a child scope using scope.$on and similarly to notify parent scope for a change, you can use `$scope.$emit'.
From parent controller,
$scope.$broadcast('eventName', a_value_or_an_object);
And in the child controller,
scope.$on('eventName', function($event, value_or_object){});
By default, $emit will cause event to be propagated towards $rootScope, which means it will first hit parent, then parent's parent scope.
And if you want to cancel further propagation, you can use $event.preventDefault() in the eventListener.
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.
To set the stage - this is not happening within a single scope, where I can bind a simple attribute. The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope). Further, the button that's clicked is in a child scope about 3 children deep from root.
Here is how I'm currently solving this:
HTML (sitting in root scope):
<ul class="nav-secondary actions"
darthFader fadeDuration="200"
fadeEvent="darthFader:secondaryNav">
Where darthFader is my directive.
Directive:
directive('darthFader',
function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.$on(attrs.fadeevent, function(event,options) {
$(element)["fade" + options.fade || "In"](attrs.fadeduration || 200);
});
}
}
})
So here I'm creating an event handler, specific to a given element, that is calling fadeIn or fadeOut, depending on an option being passed through the event bus (or defaulting to fadeIn/200ms).
I am then broadcasting an event from $rootScope to trigger this event:
$rootScope.$broadcast('darthFader:secondaryNav', { fade: "Out"});
While this works, I'm not crazy about creating an event listener for every instance of this directive (while I don't anticipate having too many darthFader's on a screen, it's more for the pattern I would establish). I'm also not crazy about coupling my attribute in my view with an event handler in both my controller & directive, but I don't currently have a controller wrapping the secondary-nav, so I'd have to bind the secondaryNav to $rootScope, which I don't love either. So my questions:
Is there a way to do this without creating an event handler every time I instantiate my directive? (maybe a service to store a stateful list of elements?)
How should I decouple my view, controller & directive?
Any other obvious questions I'm missing?
Cheers!
You mention in your question
The element I want to fade in/out does not sit inside a controller, it sits inside the ng-app (rootScope).
I believe if I were to write this same functionality, I would put the element in its own controller--controllers are responsible for managing the intersection of the view and the model, which is exactly what you're trying to do.
myApp.controller('NavController', function($scope) {
$scope.fadedIn = false;
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="fadedIn">
myApp.directive('darthFader', function() {
return {
restrict: 'A',
link: function($scope, element, attrs) {
var duration = attrs.fadeDuration || 200;
$scope.$watch(attrs.fadeShown, function(value) {
if (value)
$(element).fadeIn(duration);
else
$(element).fadeOut(duration);
});
}
};
});
If you're worried about sharing the fade in/out state between multiple controllers, you should create a service to share this state. (You could also use $rootScope and event handlers, but I generally find shared services easier to debug and test.)
myApp.value('NavigationState', {
shown: false
});
myApp.controller('NavController', function($scope, NavigationState) {
$scope.nav = NavigationState;
});
myApp.controller('OtherController', function($scope, NavigationState) {
$scope.showNav = function() {
NavigationState.shown = true;
};
$scope.hideNav = function() {
NavigationState.shown = false;
};
});
<ul ng-controller="NavController"
class="nav-secondary actions"
darthFader fadeDuration="200"
fadeShown="nav.shown">
<!-- ..... -->
<div ng-controller="OtherController">
<button ng-click="showNav()">Show Nav</button>
<button ng-click="hideNav()">Hide Nav</button>
</div>
Create a custom service, inject it in the controller. Call a method on that service that will do the fade-in/fade-out etc. Pass a parameter to convey additional information.