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.
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 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/
In this example plunker, http://plnkr.co/edit/k2MGtyFnPwctChihf3M7, the nested directives compile fine when calculating the DOM layout, but error when the directive tries to reference a variable to bind to and says the variable is undefined. Why does this happen? The data model I am using is a single model for many nested directives so I want all nested directives to be able to edit the top level model.
I havn'et got a clue as to what you're trying to do. However, your comment 'so I want all nested directives to be able to edit the top level model' indicates you want your directive to have scope of your controller. Use
transclude = true
in your directive so that your directives can have access to your the parent scope.
http://docs.angularjs.org/guide/directive#creating-a-directive-that-wraps-other-elements
I don't know why you are doing it this way exactly, it seems like there should be a better way, but here goes a stab at getting your code working. First you create an isolated scope, so the scopes don't inherit or have access to anything but what is passed in the data attribute. Note that you can have your controller set dumbdata = ... and say <div data="dumbdata" and you will only have a data property on your isolated scope with the values from dumbdata from the parent in the data property. I usually try to use different names for the attribute and the data I'm passing to avoid confusion.
app.directive('project', function($compile) {
return {
template: '<div data="data"></div>',
replace: true,
scope: {
data: '=' // problem
},
Next, when you compile you are passing variables as scopes. You need to use real angular scopes. One way is to set scope: true on your directive definition, that will create a new child scope, but it will inherit from the parent.
app.directive('outer', function($compile) {
var r = {
restrict: 'A',
scope: true, // new child scope inherits from parent
compile: function compile(tEle, tAttr) {
A better way is probably to create the new child scope yourself with scope.$new(), and then you can add new child properties to pass for the descendants, avoiding the problem of passing values as scopes and still letting you have access to the individual values you're looping over (plunk):
app.directive('outer', function($compile) {
var r = {
restrict: 'A',
compile: function compile(tEle, tAttr) {
return function postLink(scope,ele,attrs) {
angular.forEach(scope.outer.middles, function(v, i) {
var x = angular.element('<div middle></div>');
var s = scope.$new(); // new child scope
s.middle = v; // value to be used by child directive
var y = $compile(x)(s); // compile using real angular scope
ele.append(y);
});
};
}
};
return r;
});
I'm struggling to find the best solution for a collapse/expand directive that behaves like an accordion, ie. only one collapse/expand directive on the page must be open at any one time.
What is the best way to go about this, and get an expanding directive to tell the other directives to collapse? Can I use an isolated scope, a parent controller, broadcast events? Basically I'm having difficulties wrapping my head around inter-directive communication.
I know that there are accordion directives available, but I want to learn building directives myself. Thanks.
I ended up using $broadcast from a parent controller. The expanding directive asks the parent controller to broadcast a collapseChange event, which all directives listen for.
Parent controller
$scope.broadcastCollapseChange = function (id) {
$scope.$broadcast('collapseChange', { 'id': id});
};
Directive
scope.collapsed = true;
var onCollapseChange = function (v) {
if (scope.collapsed == false)
scope.$parent.broadcastCollapseChange(scope.$id);
}
scope.$watch('collapsed', onCollapseChange);
scope.$on('collapseChange', function (event, args) {
if (scope.collapsed == false && args.id != scope.$id)
scope.collapsed = true;
});
Currently I have to use $parent in the directive to get the parent controller, which is not very elegant. Is there any way I can get around this?
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.