Preface
When I declare a directive under a controller, e.g.
<div ng-controller="MyController">
<my-directive></my-directive>
</div>
the directive inherits that controller's scope by default. This means if the controller defines
$scope.Heaven = "Free Beer"
then I have access to that within the directive's template via
{{ Heaven }}
Question
When declaring a directive within another directive, why doesn't the child inherit scope like it would if placed in a controller?
<my-parent-directive>
<my-child-directive>
</my-child-directive>
</my-parent-directive>
In short, if I declare a controller function for my-parent-directive and in it write:
$scope.Heaven = "Free Beer"
My child directive doesn't have access to this by default. Why is that?
(This assumes "scope: true" within the parent, no scope declaration in the child, and the child requiring the parent via "require: 'my-parent-directive')
Example codepens:
Directive wrapped in controller
Directive wrapped in directive
Question was modified after answer was given - the below is to preserve the reference
Directive wrapped in directive old
I am looking at the "Directive wrapped in directive old" on codepen. I think it is this you want to fix, but I'm not certain since your codepen is different to the example in your question (that's not a criticism, just clarification in case I am heading down the wrong route for you!)
However, if I am correct (and I am referring to the "Directive wrapped in directive old" on codepen for the rest of this answer):
You have declared the scope in myWrapper to be inherited ("scope: true"), therefore any properties that you add to the scope within myWrapper (such as "$scope.passdown = $attrs.passdown;") will only be visible to myWrapper.
You can remove the "scope: true" from myWrapper to share the scope between everything (not a great structure to use, but it will work) and you will solve your immediate problem, if I have understood you correctly. Or you can move the "passdown" property to a mutable object on the "parent" controller "$scope.abc = {passdown: ''};" for example. Then modify the value in myWrapper: "$scope.abc.passdown = $attrs.passdown;" and access it as "abc.passdown" in your interpolated expressions.
a bit of background:
changes to immutable types in "child" controllers/directive will make a copy of the property and those changes will never be seen on any other scope.
No scope declaration means shared scope - all components that share this scope too can see any properties / changes (to mutables) made on the scope. Tends to end up with closely coupled components that become very difficult to maintain.
"scope: true" means inherited scope and any additions made to the scope will only be visible to the inherting scope (ie the "child"). Changes to mutable properties in the parent will be visible to all other components that share this scope.
"scope: {...}" creates an isolated scope and provides a safe way to expose properties to parents and let the children modify those properties. This implementation is more work but you will end up with code that is easier to understand, maintain and share.
I hope this answer isn't too rambling and it helps you solve your problem.
Related
I am facing a strange situation in which I have created a directive, to which a controller is attached, and one of the two tiny functions of the controller is never called from the view whereas the other function is.
Here is the plunker.
The message I expect is (bold is what does not show up)
You are limited to: Prison
I have already created tens of directives, whether in their own right or as wrappers around existing directives available on GitHub, from lightweight ones such as custom-select to behemoths such as angular-ui-grid.
I am at the end of my wits here as to why {{getArea()}} produces no text at all in the view. I've scrutinized the code, trying to do it with new eyes, so to speak, and I see nothing wrong. I've created a specific project in Eclipse for this tiny piece of code, installed Wampserver just so I could set breakpoints in Firebug and God knows to what great lengths I had to go just so that I could understand what is wrong with the code I wrote.
For instance, in isRestricted(), I can call getArea() without any problem. However, Angular seems to not find the function from the directive.
A few similar questions have already been asked but none of the errors (missing controller or ng-app specification, missing dependency list at module declaration, nested controllers, etc.) seem to apply. There's obviously an important lesson to be learned here and I'm truly eager to learn it.
EDIT: The lesson learned is that ng-if creates a new scope. That new scope comes in between the controller and the directive, which leads to the template of the directive losing access to anything defined in the controller (at least, that's how I would phrase it). (Note that a comment hinted at directive priority.)
There are several solutions, which all maintain the prototypical inheritance needed for the template to access the functions defined in the controller:
not using an isolate scope
not defining the ng-if directive on the top-level element of my directive, as that causes a conflict (between my controller's scope and the scope defined by ng-if). I believe ng-if wins here, which leads to the controller's scope being out of reach of the directive. Using ng-if on a child div does the trick (because then, the ng-if scope inherits my controller's scope, hence making the functions available to the template).
Because of the CSS styling needed with this directive, I have used scope: false.
<span class="scoop-badge-content">{{$parent.getArea()}}</span>
Or in directive :
scope:true
This is because ng-if use how own scope
The strange thing is that when i have this problem i usually use dot notation. But it doesn't work here, probably because we're inside a directive, and i didn't had the case until now.
EDIT : a last way of doing this chaging the template :
<div class="scoop-badge scoop-badge-ua">
<div ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I think this work because you have replace true and ng-if will conflict with ng-scope if it's on the top DOM element.
When you have scope = {} in your directive, Angular creates an isolated scope. Which means it can't get to the getArea() function.
You can completely remove the scope = {} line or set it to scope = true or scope = false depending on what you're trying to achieve later on.
When set to scope = true Angular will create a new scope object and assign it to the directive. This scope object is prototypically inherited from its parent scope.
When set to scope = false the directive will use its parent scope. (This is the default value. It has the same effect if you remove this line).
More information about scopes here
Removing scope: {} from directive definition solves problem.
app.directive('scoopBadgeUa', function() {
return {
restrict : "A",
scope: {}, // This is not needed, creates conflict
templateUrl : "scoop-badge-ua.html",
replace : true,
controller : 'ScoopBadgeUaController',
};
});
Your code is correct, you don't have to do anything more than adding a <div></div> wrapping your code in scoop-badge-ua.html.
<div>
<div class="scoop-badge scoop-badge-ua" ng-class="{'visible': isRestricted()}" ng-if="isRestricted()">
<span class="scoop-badge-title">You are limited to:</span>
<span class="scoop-badge-content">{{getArea()}}</span>
</div>
</div>
I like this solution better than transclude: true because it doesn't include the directive's tag in your html code, which ultimately translates to a cleaner code.
As far as I know that basically scope is an instance of a controller.
Every time I declare a controller scope will be available for that controller.
But then why directive has scope in link function ?
I didn't declare any controller for the directive.
Then why link function has scope ? Any Idea ?
From the doc:
scope:
The scope to be used by the directive for registering watches.
You may also be interested to see the differences between $scope and scope.
All directives have a scope associated with them. They use this scope for accessing data/methods inside the template and link function. By default, unless explicitly set, directives don’t create their own scope. Therefore, directives use their parent scope ( usually a controller ) as their own.
However, AngularJS allows us to change the default scope of directives by passing a configuration object known as directive definition object. A directive definition object –– let’s call it as DDO –– is a simple JavaScript object used for configuring the directive’s behaviour,template..etc. Check out AngularJS docs about DDO.
So, we can use scope inside linking function to work with parent scope, child scope or isolated scope.
You may visit this for detailed information about scope inside directive.
You can share the data between controller and linking function.
Sometimes we would like to access that same data in both controller and link functions in Angular directive. Usually we add this data to the scope object. This has certain problems, for example exposing it to any child scope. Add the shared properties to the controller instance itself and access in the link function.
You may also be interested in what is need of link function?
A directive is Angular's way of defining 'components': you specify how its markup looks and how it behaves before, during and after being rendered. The link functions allow you to hook up events to the DOM element before or after it gets 'linked' (see also: pre-link and post-link methods). When these events are fired you might want to change some of the variables within the scope, and that's why you have access to it within the link functions.
In the video AngularJS MTV Meetup: Best Practices (2012/12/11), Miško explains "..if you use ng-model there has to be a dot somewhere. If you don't have a dot, you're doing it wrong.."
However, the very first example (The Basics) in the Angular.JS website seems to contradict it. What gives? Has Angular.JS changed since the MTV meetup that it's now more forgiving with ng-model?
That little dot is very important when dealing with the complexities of scope inheritance.
The egghead.io video "The Dot" has a really good overview, as does this very popular stack overflow question: What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
I'll try to summarize it here:
Angular.js uses scope inheriting to allow a child scope (such as a child controller) to see the properties of the parent scope. So, let's say you had a setup like:
<div ng-controller="ParentCtrl">
<input type="text" ng-model="foo"/>
<div ng-controller="ChildCtrl">
<input type="text" ng-model="foo"/>
</div>
</div>
(Play along on a JSFiddle)
At first, if you started the app, and typed into the parent input, the child would update to reflect it.
However, if you edit the child scope, the connection to the parent is now broken, and the two no longer sync up. On the other hand, if you use ng-model="baz.bar", then the link will remain.
The reason this happens is because the child scope uses prototypical inheritance to look up the value, so as long as it never gets set on the child, then it will defer to the parent scope. But, once it's set, it no longer looks up the parent.
When you use an object (baz) instead, nothing ever gets set on the child scope, and the inheritance remains.
For more in-depth details, check out the StackOverflow answer
Dot will be required when you prototypically inherit one scope from another for example in case of ng-repeat a new scope is created for every line item which prototypically inherits from parent scope. In the basic example there is no prototype inheritance since there is only one scope but if you have a number of child scopes then you will start facing the problem. The link below will make everything clear.
https://github.com/angular/angular.js/wiki/Understanding-Scopes#ng-repeat
So to solve this, make sure in the JS you declare the parent first:
e.g.
$scope.parent
followed by:
$scope.parent.child = "Imma child";
doing just the child without the parent will break Angular.
According to #OverZealous's answer, I thought up a dirty but comfortably simple and quick workaround for this:
$scope.$s = $scope
$scope.foo = 'hello'
Then use $s in template can safely modify model:
<input ng-model="$s.foo"/>
I wrote a service for such dirty works in my project.
Related Post, but didn't help:
Scoping issue when setting ngModel from a directive
EDIT: Can I use ng-model with isolated scope? didn't work either.
I got the some problem but in a more complex way I guess. I want to write a pulldown that does not use any input for data saving. I'd rather have the ngModel to take care of it.
http://jsfiddle.net/QeM6g/6/
The jsFiddle example above shows a demo where the above described methods didn't work.
// this is what should work but doesn't
ngModel.$setViewValue(value);
scope.$apply(attr.ngModel,value);
For some reason the scope of the ngModelController is a sibling of my scope. so it doesn't pass the changes back to the parent. at least all other sibling scopes behave as you'd expect. i.e. ng-change works in combination.
Angularjs doesn't deal very well with direct bindings to primitive types.
If you change this line:
$scope.productId = 16;
to something like this:
$scope.selectedProduct = {
id: 16
}
and change those references on the rest of the code, you should be able to overcome the issue.
jsFiddle: http://jsfiddle.net/M2cL7/
Don't bind to primitives in a scope, bind to an object in the scope.
From https://github.com/angular/angular.js/wiki/Understanding-Scopes
... until you try 2-way data binding
(i.e., form elements, ng-model) to a primitive (e.g., number, string,
boolean) defined on the parent scope from inside the child scope. It
doesn't work the way most people expect it should work. What happens
is that the child scope gets its own property that hides/shadows the
parent property of the same name. This is not something AngularJS is
doing – this is how JavaScript prototypal inheritance works. New
AngularJS developers often do not realize that ng-repeat, ng-switch,
ng-view and ng-include all create new child scopes, so the problem
often shows up when these directives are involved.
This issue with primitives can be easily avoided by following the
"best practice" of always have a '.' in your ng-models
so
<input ng-model="tweetText">
becomes
<input ng-model="tweet.text">
A great summary is here:
https://www.youtube.com/watch?v=ZhfUv0spHCY&feature=youtu.be&t=30m
To get a grip on AngularJS I decided to play around with one of the examples, specifically, simply adding a "complete" screen to the Todo-example, when the user has entered 5 todos it uses a switch-case to switch to another div. Code is available here http://jsfiddle.net/FWCHU/1/ if it's of any use.
However, it appears that each switch-case gets its own scope ($scope.todoText is not available), however it can be accessed using "this" from within addTodo() in this case. So far so good, but say I want to access todoText (which is inside the switch-case) outside of the switch-case, how would I go about doing that? Can I bind the switch-case scope to the model perhaps, is it accessible in some other way or should this be solved in some other way?
PS. I'm not trying to find ANY solution to the code above, I'm pretty sure I know how to solve it without using switch-cases, I want to understand how scopes work in this case!
Mark has some great suggestions! Make sure you also check out the AngularJS Batarang Chrome Extension to see the various scopes and their values (among other things). Note it doesn't appear to work well with jsFiddle.
I'm not sure how to access inner scopes directly but here is one way to access the same text in the outer scope by binding to an object instead of a primitive.
1) Declare todoText as an object instead of a primitive in your controller:
$scope.todoText = {text: ''};
2) Bind to todoText.text instead of just todoText:
<form ng-submit="addTodo()">
<input type="text" ng-model="todoText.text" size="30" placeholder="add new todo here">
<input class="btn-primary" type="submit" value="add">
</form>
3) Modify the existing functions to use todoText.text:
$scope.addTodo = function() {
$scope.todos.push({text:$scope.todoText.text, done:false, width: Math.floor(Math.random() * 100) + 50});
$scope.todoText.text = '';
};
Take a look at this fiddle and note that the text displayed beneath the textbox when you type something in is accessing the todoText.text on the outside scope.
If you change the code back to use a primitive (like in this fiddle) the parent scope todoText won't reflect any changes you make to the textbox. This is likely more to do with how JavaScript copies reference values (see this post for more info) and less an AngularJS specific thing.
Update2: Now that I know a little more about AngularJS, here's a much better answer.
say I want to access todoText (which is inside the switch-case)
outside of the switch-case, how would I go about doing that?
There is no way for parent scopes to access child scopes. (One reason for this restriction, according to Angular developers, is for easier memory management of scopes.) (Well, you could use $$childHead and $$childTail to access child scope, but you shouldn't!)
Can I bind the switch-case scope to the model perhaps, is it
accessible in some other way or should this be solved in some other
way?
There are three common ways to access the parent model from the child scope:
Do what #Gloopy suggests: create an object in the parent scope, then refer to properties on that object in the child scope.
Use $parent in the child scope to access the parent scope and its properties, even primitive properties.
Call a method on the parent scope
To convert your fiddle to use $parent:
<input type="text" ng-model="$parent.todoText" ...
$scope.addTodo = function() {
$scope.todos.push({text: $scope.todoText, ...
$scope.todoText = '';
As I mentioned in the comments on Gloopy's answer, ng-repeat and ng-switch both have the new child scope prototypically inherit from the parent scope. ng-repeat also copies the loop variable/item to the new child scope (and the nuances that #Gloopy describes with primitives vs object applies). ng-switch does not copy anything from the parent scope.
To see what the inner/child scope looks like, add the following after the ng-switch-when:
<a ng-click="showScope($event)">show scope</a>
and add this to your controller:
$scope.showScope = function(e) {
console.log(angular.element(e.srcElement).scope());
}
Update1: (strikethroughs added to bad advice, []'s added for clarity)
For this scenario, where AngularJS is creating additional inner scopes (implicitly), and you don't really want/need another controller, I like Gloopy's solution. A service (what I originally suggested below) is [the wrong way to do this] probably overkill here. I also like that Gloopy's solution does not require the use of 'this' in the controller methods.
Original Answer: (strikethroughs added to bad advice, []'s added for clarity)
To see where scopes are being created (if you haven't tried this already, it is handy):
.ng-scope { margin: 4px; border: 1px dashed red }
To access todoText outside the switch-case (hence outside of its scope), you're essentially asking about inter-controller communication, since multiple scopes are involved. You have a few options, but a service is probably best. Store the data (that needs to be shared) inside the service, and inject that service into each controller that needs access to the data.
For your specific example, I think you'd need to attach a controller to each switch-case and inject the service into it, to get access to the shared data.
See also AngularJS: How can I pass variables between controllers?.
The other options:
Using $scope.$parent in the inner scope is [one way to do this -- see Update2 above] not recommended, since then a controller would be making assumptions about how the data is presented.
Using $rootScope is not recommended, except maybe for simple, one-off applications. That shared data may start to take on a life of its own, and $rootScope is not the place for that to happen. Services are easier to reuse, to add behavior to, etc.
Using $scope.$emit is another option, but it seems messy and a bit odd: emitting events to share data (instead of triggering behavior).
[Using an object in the parent scope is probably best -- see #Gloopy's answer.]