I have 3 nested, isolate scope, directives (see CodePen here) and I am able to pass a function (that takes an argument) from the outermost directive to the innermost directive (passing the function from outer directive to intermediate directive to inner directive).
What I'm failing to understand is what needs to be done to pass the argument from the inner directive back through intermediate directive back to the outer directive.
Again, see the CodePen example.
Note: Given only 2 isolate scope directives I can get this to work with something similar to the following...
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName(name)"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
template: "<span ng-click='addName({name: testName})'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
The above code gives me an Alert box with 'Henry' (just like I'd expect).
It's when I add an third, intermediate, isolate scope directive that I run into problems...
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName(name)"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
template: '<div><my-dir-3 add-name="addName({name: testName})"></my-dir-3></div>',
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '&'
},
template: "<span ng-click='addName({name: testName})'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
This code gives me an alert box with undefined...
A common misconception is that "& is for passing functions". This isn't technically correct.
What & does is create a function on the directive scope that, when called, returns the result of the expression evaluated against the parent scope.
This function takes an object as an argument that will override local variables in the expression with those from the directive scope (the {name: testName}) object in this case.
If you were to look under the hood, the $scope.addName method in myDir2 would look like this (simplified):
$scope.addName = function(locals) {
return $parse(attr.addName)($scope.$parent, locals);
}
Your second directive works because the expression it is binding to is
addName(name)
This expression has a local variable name, that is overridden with the value of testName from the directive when executed with
addName({name: testName}) //in the directive.
Remember - the addName function in myDir2 IS NOT the same as the addName function in myDir1. It is a new function that evaluates the expression
addName(name)
against the parent scope and returns the result.
When you apply this logic to myDir3, the expression that is evaluated is:
addName({name: testName})
Note that the only local variable in this expression is "testName". So when you call in myDir3 with
addName({name: testName})
there is no local variable name to override, and testName is left undefined.
Phew! No wonder this confuses JUST ABOUT EVERYBODY!
How to fix in your example:
You want the expressions to evaluate to the actual function in myDir1.
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name); // alerts 'Henry'
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '&'
},
// addName() returns the actual function addName from myDir1
template: '<div><my-dir-3 add-name="addName()"></my-dir-3></div>',
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '&'
},
//calls addName() on myDir2, which returns addName from myDir1, then invokes it passing testName as an argument
template: "<span ng-click='addName()(testName)'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = 'Henry';
}
}
});
Here is the working Pen
Final note - the reason why '&' is more appropriate than '=' here is that '=' is going to actually set up a $watch and two-way bind the variables between the directives. This means that myDir2 could actually change the function appName in myDir1, which is not required and undesirable. It also requires setting up two $watchs, which I try to avoid for performance reasons in Angular.
There are two ways to pass a function in an isolated scope. While '&' will make sure that what you are passing is in-fact a function, you can also pass a function as a bound variable with '=', and invoke it only when you need. This method has drawbacks, but it will leave the control of the invocation to the component that is in-charge of that invocation.
Your codepen working
angular.module('myApp', [])
.directive('myDir1', function() {
return {
template: '<div><my-dir-2 add-name="addName"></my-dir-2></div>',
controller: function($scope) {
$scope.addName = function(name) {
alert(name);
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '='
},
template: '<div><my-dir-3 add-name="addName"></my-dir-3></div>'
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '='
},
template: "<span ng-click='addName(testName)'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = "Henry"
}
}
});
The problem with your code with three nested directives is that testName does not exist in isolated scope of myDir2. You can set that from myDir3, but for that you will have to create an object in scope of myDir2 and set its property to testName and then use that property in myDir2
The working example is here
http://codepen.io/anon/pen/Wveqwx
'myDir2' when you have 3 directives is referencing 'testName', but it doesn't exist in that scope. It was mapped to 'name' in the 'myDir3'. Because of this, changing the template in myDir2 as follows, should fix it:
template: '<div><my-dir-3 add-name="addName({name: name})"></my-dir-3></div>'
You can access an elements scope using isolateScope()
In the myDir1 directive replace the controller to this instead
link: function($scope, elem) {
$scope.addName = function(name) {
var e = elem.find('my-dir-3');
var s = e.isolateScope();
alert(s.testName);
};
Another way is to potentially use $$nextSibling to get the next scope after current parent (haven't really used this so may want to read up on it)
This is probably not the 'angular way' to do things. I think removing the isolated scope from the child directives and having them reference the parent directive model would be more ideal.
Related
I'm trying to figure out AngularJS directives. I've got the following JSFiddle with an example of something I'm trying to do. https://jsfiddle.net/7smor9o4/
As you can see in the example, I expect the vm.alsoId variable to be equal to vm.theId. In the template vm.theId displays the correct value but vm.alsoId does not.
What am I doing wrong? How could I accomplish my goal.
If it helps the final idea is to execute something as the following:
function directive(service) {
var vm = this;
vm.entity = null;
init();
function init() {
service.getEntity(vm.theId).then(function (entity) {
vm.entity = entity;
});
}
}
As you've noticed, the bindToController bindings are not immediately available in the controller's constructor function (unlike $scope, which are). What you're looking for is a feature introduced with Angular 1.5: Lifecycle Hooks, and specifically $onInit.
You had the right idea; simply replace your init function definition and invocation as follows:
vm.$onInit = function () {
service.getEntity(vm.theId).then(function (entity) {
vm.entity = entity;
});
};
And here is your updated fiddle.
(Alternatively, without this solution, you'd have needed a watch.)
Angular recommends that you bind a controller "only when you want to expose an API to other directives. Otherwise use link."
Here's a working fiddle using the link function.
angular.module('app', [])
.directive('directive', directive);
angular.element(function() {
angular.bootstrap(document, ['app']);
});
function directive() {
return {
restrict: 'E',
scope: {
theId: '<'
},
template: `
alsoId: <span ng-bind="alsoId"></span>
theId: <span ng-bind="theId"></span>`,
link: link
};
}
function link(scope, element, attrs) {
init();
function init() {
scope.alsoId = scope.theId;
}
}
Im learning about Angular directives and I can't wrap my head around the scope topic. Suppose I have this custom directive which is named parentDirective. It has a controller property and a link property, as follows:
angular.module("app").directive("parentDirective", function () {
return {
restrict: "E",
templateUrl: "dirs/parent.html",
scope:{
character: "="
},
controller: function ($scope) {
$scope.getData = function (data) {
console.log(data);
}
},
link: function (scope,elem, attrs) {
elem.bind("click", function (e) {
//get object here?
});
scope.getData = function (data) {
console.log(data);
}
}
}
});
Its template is defined as follows:
<p ng-click="getData(character)">
{{character.name}}
</p>
I can get the character object in the controller function through the $scope variable and I have access to the same data in the link function through scope. Whats the difference between the two methods in this regard? Second question, Is it possible to bind a click to the directive and get the object like this:
elem.bind("click", function (e) {
//get object here?
});
Scope is specific to current directive instance and is the same object in both functions.
For defining methods on the scope, there's no difference if they are defined in controller or link function, unless there is race condition that requires the method to be defined as early as possible. For this reason it makes sense to define scope methods in controller.
Event handler doesn't differ from any other function, it is
elem.on("click", function (e) {
scope.$apply(function () {
scope.character...
});
});
scope.$apply(...) wrapper doesn't hurt anyway, but the necessity of it depends on what happens with scope.character.
The directive can have only controller and no link. Current Angular versions (1.5+) suggest the style where bindToController + controllerAs are used instead of scope bindings as common ground for directives and components.
Then the directive may look like
restrict: "E",
template: '<p>{{$ctrl.character.name}}</p>',
controllerAs: '$ctrl',
bindToController: { character: "=" },
controller: function ($element, $scope) {
var self = this;
self.getData = function (data) { ... };
$element.on("click", function (e) {
scope.$apply(function () {
self.character...
});
});
}
link function may appear as $postLink controller hook, but here it is not needed.
I've followed Angular docs precisely to get a directive working with an isolated scope containing a couple of vars from the parent Controller's scope object.
app.controller('MainCtrl', function($scope) {
$scope.name = 'Parent Name';
$scope.pokie = {
whyIs: "thisUndefined?"
};
});
app.directive('parseObject', function() {
var preLink = function($scope, el, att, controller) {
console.log('[link] :: ', $scope);
};
var postLink = function($scope, el, att, controller) {
console.log('[PostLink] :: ', $scope);
console.log('[$Parent] :: ', $scope.$parent.name);
};
return {
restrict: 'E',
scope: {
myPokie: '=pokie',
name: '=name'
},
template: [
'<div>',
'<h1>Directive does not get parent scope</h1>',
'<h1>{{ myPokie }}</h1>',
'<h2>{{ name }}</h2>',
'</div>'
].join(''),
compile: function() {
return {
pre: preLink,
post: postLink
}
}
}
});
http://plnkr.co/edit/FpQtt9?p=preview
Can anyone tell me what is wrong with my code? Why does the directive's Isolate scope return undefined values for 'myPokie' and 'name'?
I've seen other people saying you have to use $scope.watch for this.. but angular's directive docs don't say anything about that.. And I really don't want to use $scope.watch for something so trivial which should work out of the box.
As you declared isolated scope in your directive, that means you are going to provide those value to your directive using attribute.
scope: {
myPokie: '=pokie',
name: '=name'
}
This means your directive scope will not be prototypically inherited from the parent scope. pokie attribute provide the value for myPokie & name attribute will provide value of name for your directive, = indicating a two way binding if your myPokie value change in directive, the same referencing value will change in the parent controller. The same is true for the name attribute.
Your directive element markup should be:
<parse-object pokie="pokie" name="name"></parse-object>
Working Plunkr
You're using isolated scope but you're not passing the variables. The problem is in your HTML:
Change your HTML from:
<parse-object></parse-object>
To:
<parse-object pokie="pokie" name="name"></parse-object>
Isolated scope takes its parameters from the DOM element. So if you have inside the scope declaration:
myPokie: '=pokie',
That means that myPokie variable should be taken from the pokie attribute that's on the scope. Your name: "=name" can be changed to name: "=" since it is exactly the same name.
Plunker
I'm setting up a directive which can't have an isolated scope because there are going to be other directives (which are 3rd party) which are already using it. I need to get the reference of a third party object and pass it to the controller as a variable. How can I do this?
angular.module('myModule')
.directive('objectSync', ['$thirdParty', function ($thirdParty) {
return {
compile: function() {
return {
pre: function(scope, element, attrs) {
var thirdPartyObject = $thirdPartyObject.getCurrentScopeObject();
scope[attrs.objectSync] = thirdPartyObject;
}
};
}
};
}]);
And this is the html:
<third-party-directive
sync-object-to-controller="objectToSync">
When I do this, objectToSync never makes it to the controller's scope. But if I set up a container object in the scope: $scope.containerObject = {}, in the directive instead of adding it right to the scope I add it there: scope.containerObject[attrs.objectSync] = thirdPartyObject;, then it makes it through, but I can only call it/attach listeners to it using watch: $scope.$watch(function(){ return $scope.containerObject; } ...
So how can I pull an object from a third party from within the scope of the third party directive and pass it into the root of the controller's scope with a name specified in my own directive before the controller runs?
You need to receive the parameter passed to third-party-directive.
angular.module('myModule')
.directive('objectSync', ['$thirdParty', function ($thirdParty) {
return {
scope : {syncObjectToController : '='} //this receives the parameter
//the hyphen separated name gets converted to camel casing
compile: function() {
return {
pre: function(scope, element, attrs) {
//now you can directly use the third party object here directly
var thirdPartyObject = scope.syncObjectToController
}
};
}
};
}]);
Read more about it in angular's documentation of directives.
I am developing a directive which shows and hides it's contents based on a click event (ng-click) defined in it's template. On some views where the directive is used I'd like to be able to know if the directive is currently showing or hiding it's contents so I can respond to the DOM changes. The directive has isolated scope and I am trying to notify the parent scope when the directive has been "toggled". I'm attempting to accomplish this by passing a callback function to the directive where it is used that can be called when the directive's state changes i.e hides or shows
I'm not sure how to correctly implement this being that the state of the directive (hidden or shown) is stored in the directive's isolated scope and is determined after the ng-click. Therefore I need to call the parent scope's function from within the directive and not from withing the view.
This will make WAAY more sense with an example. Here is a plunked demonstrating what I'd like to do:
http://plnkr.co/edit/hHwwxjssOKiphTSO1VIS?p=info
var app = angular.module('main-module',[])
app.controller('MainController', function($scope){
$scope.myValue = 'test value';
$scope.parentToggle = function(value){
$scope.myValue = value;
};
});
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="toggle();">Click Me</a>',
replace: true,
scope: {
OnToggle: '&'
},
link: function($scope, elem, attrs, controller) {
$scope.toggleValue = false;
$scope.toggle = function () {
$scope.toggleValue = !$scope.toggleValue;
$scope.OnToggle($scope.toggleValue)
};
}
};
});
I'm relatively new to Angular. Is this a bad idea to begin with? Should I be using a service or something rather than passing around function refs?
Thanks!
Update
You can also use & to bind the function of the root scope (that is actually the purpose of &).
To do so the directive needs to be slightly changed:
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="f()">Click Me</a>',
replace: true,
scope: {
toggle: '&'
},
controller: function($scope) {
$scope.toggleValue = false;
$scope.f = function() {
$scope.toggleValue = !$scope.toggleValue;
$scope.toggle({message: $scope.toggleValue});
};
}
};
});
You can use like this:
<div toggle="parentToggle(message)"></div>
Plunk
You could bind the function using =. In addition ensure the property name in your scope and tag are matching (AngularJS translates CamelCase to dash notation).
Before:
scope: {
OnToggle: '&'
}
After:
scope: {
onToggle: '='
}
Furthermore don't use on-toggle="parentToggle({value: toggleValue})" in your main template. You do not want to call the function but just passing a pointer of the function to the directive.
Plunk