Why doesn't scope pass through properly to nested directives? - angularjs

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;
});

Related

Angular scopes and binding data to a directive

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.

AngularJS : Child directive scope to inherit from parent directive isolated scope?

If a and b are my directives such that b is a child element of a :
<a>
<b></b>
</a>
Is it possible that if a has an isolated scope, then b could inherit from it?
Example js:
app.directive('a', function () {
return {
restrict: 'E',
scope: {},
controller: function ($scope) {
$scope.prop1 = ...
}
}
});
app.directive('b', function () {
return {
restrict: 'E',
controller: function ($scope) {
//how to access $scope.prop1 here?
}
}
});
With this, I'm trying to make directives that are reusable and are supposed to be used as nested within each other.
I know that I can require the controller of a on directive b to access it within the link function of b as one way to share the data between controllers, but that approach isn't working very well if I have more than one level of nesting.
This is where you need to use the manual transclusion function. If the parent directive has an isolate scope, the child DOM elements (and their directives) would not inherit from it (only, if they were in its template).
When you transclude, you can specify the scope explicitly:
.directive("a", function(){
return {
scope: {},
transclude: true,
link: function(scope, element, attrs, ctrls, transclude){
var newScope = scope.$new();
transclude(newScope, function(clone){
element.append(clone);
})
}
};
});
You should note, though, that although the above would work (in the sense that the child directive's scope would inherit the parent's isolate scope), it is also a somewhat confusing experience to the user of your directive.
To see why, imagine that a exposes some $innerProp on its scope. The user of a now has to know that such property is "magically" available. This makes the HTML less readable without knowing a lot about a:
<a>
<b item="$innerProp"></b>
</a>
Addendum
Depending on your use case, there might be other approaches that are more suitable. The above approach works better when a and b are independent, and when a uses its contents to allow its user to specify some template.
If b is only (or mostly) used as a child of a, then it should require it. a can expose whatever it needs via its controller API to b.
Lastly, if a has a well-defined structure, then it should use its template to specify b. In your example, this could easily be achieved with a template.

How to refactor directive and change functionality by checking parent scopes?

I have a form with a ton of duplicate functionality in 2 different Controllers, there are slight differences and some major ones in both.
The form sits at the top of a products view controller, but also inside of the products modal controller.
Test plunker: http://plnkr.co/edit/EIW6xoBzQpD26Wwqwwap?p=preview
^ how would you change the string in the console.log and the color of the button based on parent scope?
At first I was going to create a new Controller just for the form, but also the HTML was being duplicated, so decided to put that into a Directive, and just add the Controller code there.
My question now is this: How would I determine which parent scope the form-directive is currently being viewed in? Because depending on the parent scope the functions/methods behave differently.
So far I've come up with this:
.directive('productForm', function() {
return {
templateUrl: "views/products/productForm.html",
restrict: "E",
controller: function($scope) {
console.log('controller for productForm');
console.log($scope);
console.log($scope.$parent);
/*
If parent scope is the page, then this...
If parent scope is the modal then this instead...
*/
}
}
});
However it's giving me back $parent id's that look like 002 or 00p. Not very easy to put in if / else statements based on that information.
Have you guys run into this issue before?
You can define 'saveThis' in your controller and pass it to directive using '&'
scope: {
user: '=',
saveThis : '&'
},
please see demo here http://plnkr.co/edit/sOY8XZtEXLORLmelWssS?p=preview
That gives you more flexibility, in future if you want to use saveThis in another controller you can define it inside controller instead adding additional if statement to directive.
You could add two way binding variables in the directive scope, this allows you to specify which Ctrl variable gets bound to which directive variable
<my-directive shared="scopeVariable">
this way you achieve two way binding of the scopeVariable with the shared directive variable
you can learn more here
I advice against this practice and suggest you to isolate common logics and behaviours in services or factories rather than in directives
This is an example of a directive that has isolated scope and shares the 'title' variable with the outer scope.
You could declare this directive this way:
now inside the directive you can discriminate the location where the directive is defined; just replace the title variable with a location variable and chose better names.
.directive('myPane', function() {
return {
restrict: 'E',
scope: {
title: '#'
},
link: function(scope, element, attrs, tabsCtrl) {
},
templateUrl: 'my-pane.html'
};
});

Call method on Directive to pass data to Controller

So basically I have a controller, which lists a bunch of items.
Each item is rendering a directive.
Each directive has the ability to make a selection.
What I want to achieve is once the selection has been made, I want to call a method on the controller to pass in the selection.
What I have so far is along the lines of...
app.directive('searchFilterLookup', ['SearchFilterService', function (SearchFilterService) {
return {
restrict: 'A',
templateUrl: '/Areas/Library/Content/js/views/search-filter-lookup.html',
replace: true,
scope: {
model: '=',
setCriteria: '&'
},
controller: function($scope) {
$scope.showOptions = false;
$scope.selection = [];
$scope.options = [];
$scope.selectOption = function(option) {
$scope.selection.push(option);
$scope.setCriteria(option);
};
}
};
}]);
The directive is used like this:
<div search-filter-lookup model="customField" criteria="updateCriteria(criteria)"></div>
Then the controller has a function defined:
$scope.updateCriteria = function(criteria) {
console.log("Weeeee");
console.log(criteria);
};
The function gets called fine. But I'm unable to pass data to it :(
Try this:
$scope.setCriteria({criteria: option});
When you declare an isolated scope "&" property, angular parses the expression to a function that would be evaluated against the parent scope.
when invoking this function you can pass a locals object which extends the parent scope.
It's a common mistake to think that $scope.setCriteria is the same as the function inside the attribute. If you log it you'll see it's just an angular parsed expression function which have the parent scope saved at it's closure.
So when you run $scope.setCriteria() you actually evaluate an expression against the parent scope.
In your case this expression happens to be a function but it could be any expression.
But you don't have a criteria property on the parent scope, that's why angular let you pass a locals object to extend the parent scope. e.g. {criteria: option}
Extends the parent scope
you wrote in a comment that it requires the directive to have knowledge of the parameter name defined in the controller. No it doesn't, it just extends the parent scope with a criteria option, you can still use any expression you want though you are provided with an extra property you may use.
A good example would be ngEvents, take ng-click="doSomething($event)":
ngClick provides you with a local property $event, you don't have to use but you may if you need.
the directive doesn't know anything about the controller, it's up to you to decide which expression you write, cheers.
You can pass the function in using =...
scope: {
model: '=',
setCriteria: '='
},
controller: function($scope) {
// ...
$scope.selectOption = function(option) {
$scope.selection.push(option);
$scope.setCriteria(option);
};
}
<div search-filter-lookup model="customField" criteria="updateCriteria"></div>

angularjs inheriting scope in nested directives

example in: http://jsfiddle.net/avowkind/PS8UT/
I want a nested child directive to get its data from its wrapping parent directive if present, otherwise from the outer controller.
<div ng-controller="MyCtrl">
<parent index="1">
<child></child>
</parent>
<parent index="2">
<child></child>
</parent>
<h1>No Parent</h1>
<child></child>
</div>
<hr>
Desired output
Parent 1
Child of parent 1
Parent 2
Child of parent 2
No Parent
Child of parent 0
Currently my child object only sees the outer controller value:
Actual output
Parent 1
Child of parent 0
Parent 2
Child of parent 0
No Parent
Child of parent 0
This is the simple version; in reality the outer directives get data from a server that is formatted by the nested child so what is communicated is a complex object not a simple string.
Furthermore the child is a visualisation that will work on different data sets so the outer parent directive is not always the same type.
More generally the pattern I am trying to get here is to have separate directives for populating the model and viewing it. so a more realistic usage would be
<temperature-for city="Auckland">
<plot/>
<analysis/>
</temperature-for>
<humidity-for city="Hamilton">
<plot/>
<analysis/>
</temperature-for>
<test-data>
<plot/>
</test-data>
A different approach which I personally have had great success using is to define the plot and analysis directives as isolate scopes, and then two-way bind the required input.
This way the directive are completely standalone components, with a explicit, defined interface. I personally made a plotting directive like this:
<plot data="countries['Auckland'].plot.data" options="countries['Auckland'].plot.options" legend="external" zoom="xy"></plot>
Scope would look like:
scope: {
data: '=',
options: '=',
zoom: '#?', // '?' indicates optional
legend: '#?',
}
This way there's no confusion what data is required for this component to work, and you can write documentation inside the directive for the desired input attributes.
All in all, this is a pattern which works very well for a large portion of use cases in AngularJS, i.e. whenever there is a case for reusability.
Edit:
Just wanted to add to that: Looking at your HTML, there's absolutely no indication what those directives use, they could depend on anything (e.g. do they get all the data from a service? or do they depend on a parent scope? If so, what scope?)
There are a couple different ways to do this but assuming that you truly want to use the parent scopes here is a solution to go along with your fiddle.
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.index = 0;
}
myApp.directive('parent', function () {
return {
transclude: true,
scope: {
index: '='
},
restrict: 'EA',
template: '<h2>Parent {{ index }}</h2>',
compile: function(tE, tA, transcludeFn) {
return function (scope, elem, attrs) {
elem.append(transcludeFn(scope)[1]);
};
}
}
});
myApp.directive('child', function () {
return {
restrict: 'EA',
scope: false,
template: '<p>Child of parent {{ index }}</p>'
}
});
You can see a fork of your fiddle here.
The idea is that by getting rid of the ngTranscludeDirective and manually creating the transclusion, you can link the transclusion with the scope of your choosing. Then you can append the result where ever you like in the element resulting from your directive's compilation.
The other main point is to make sure the child directive doesn't create a scope (at all, whether an isolate scope, transcluded scope, or new scope).
I think this will give you the results you're asking for.
NOTE: Study your scopes well, because tweaking these behaviors can have unexpected results.
For example, if you add a linking function to the child directive and it sets index to 5:
link: function(scope) {
scope.index = 5;
}
this will not affect scope.items for children nested in the parent. However it WILL affect the an external parent scope (in this case MyCtrl's scope). Any directive not inside a parent directive will just keep altering the MyCtrl index.
However if you add a new property to scope in the child link function:
link: function(scope) {
scope.somethingElse = foo;
}
scope.somethingElse will be available on the parent scope whether nested in a parent directive or not.
You'll need to transclude and fine grain it according to your parent directive scope, you can't do it using the ng-transclude directive: http://jsfiddle.net/PS8UT/2/
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.index = 0;
}
myApp.directive('parent', function ($compile) {
return {
scope: {
index: '#'
},
restrict: 'EA',
transclude: true,
template: '<h2>Parent {{ index }}</h2>',
link: function (scope, elem, attrs, ctrl, transclude) {
transclude(scope, function(clone, s){
elem.append($compile(clone)(s)); // basically you need to reassign the inner child scope to your current isolated scope
});
}
}
});
myApp.directive('child', function () {
return {
//scope: true, // no need to create another scope since you want to use the parent
// scope: { }, // no index printed
restrict: 'EA',
template: '<p>Child of parent {{ index }}</p>'
}
});
Usually, when you are dealing with templates and transclusion, needing parent inheritage, is a pain. ng-transclude won't use your immediate parent scope as the parent, it usually use your controller scope. It's stated in angular docs $compile docs:
transclude
compile the content of the element and make it available to the
directive. Typically used with ngTransclude. The advantage of
transclusion is that the linking function receives a transclusion
function which is pre-bound to the correct scope. In a typical setup
the widget creates an isolate scope, but the transclusion is not a
child, but a sibling of the isolate scope. This makes it possible for
the widget to have private state, and the transclusion to be bound to
the parent (pre-isolate) scope.

Resources