Behaviour bindToController in child scope versus isolated scope - angularjs

I was playing around with the bindToController option for directives. I stumbled upon a seemingly strange difference between the behaviour using a child scope compared to an isolated scope. When I use an isolated scope, an new scope is created for the directive, but changes to the bound controller attributes are forwarded to the parent scope. Yet when I use a child scope instead, my example breaks. (Using bindToController using child scopes should be allowed according to http://blog.thoughtram.io/angularjs/2015/01/02/exploring-angular-1.3-bindToController.html#improvements-in-14 )
The code:
{
restrict: 'E',
scope: {},
controller: 'FooDirCtrl',
controllerAs: 'vm',
bindToController: {
name: '='
},
template: '<div><input ng-model="vm.name"></div>'
};
Working demo https://jsfiddle.net/tthtznn2/
The version using a child scope:
{
restrict: 'E',
scope: true,
controller: 'FooDirCtrl',
controllerAs: 'vm',
bindToController: {
name: '='
},
template: '<div><input ng-model="vm.name"></div>'
};
Demo: http://jsfiddle.net/ydLd1e00/
The changes to name are forwarded to the child scope, but not to the parent scope. This in contrast to binding to an isolated scope. Why is this?

This is because you are using the same alias for both controllers (and has nothing to do with object vs string value as mentioned in the comments).
As you might know, the controller as alias syntax is merely creating an alias property on scope and sets it to the controller instance. Since you are using vm as the alias in both cases (MainCtrl and FooDirCtrl) you are "shadowing" the MainCtrl alias in the case of the normal child scope.
(In this context, "normal" means "prototypally inheriting from parent scope".)
Thus, when you are trying to evaluate vm.name (vm for MainCtrl) on the new scope to get the "parent value", it is actually evaluating FooDirCtrl.name (which is undefined) and the same happens when you are trying to assign back to the parent scope.
The isolate scope version is not affected, since the scope is not inheriting from it's parent scope.
Updated fiddle
UPDATE:
Taking a closer look at the source code, this might be a bug (because support for bindToController on non-isolate scopes was added "retroactively").
We seem to have been getting away with this bug, because of the prototypal inheritance, but when the names collide we are out of luck.
I have to take a closer look to make sure if it's indeed a bug and if it's "fixable", but for now you can work around it by using different aliases for your controllers (see fiddle above).
That's (part of) why I don't like using vm as my conroller alias (despite it being suggested by popular style-guides) :)
Let's track this in angular.js#13021.

Related

Priority between custom and built-in directve

I am reading ng-book-r27.
There are some something i can't understand well.
About 'Scope Option' of The chapter 'Directives Explained'.
First confusion:
If multiple directives on an element provide an isolate scope, only
one new scope is applied. Root elements within the template of a
directive always get a new scope; thus, for those objects, scope is
set to true by default.
I think that mean other directives will use the isolate scope as theirs.
is that right?
Second confusion:
example of inherited scope
ng-init has higher priority than custom directive.
Why the expression of ng-init will use the scope of custom dirctive.
I find a recommendation from offical doc about ng-init:
This directive can be abused to add unnecessary amounts of logic into
your templates. There are only a few appropriate uses of ngInit, such
as for aliasing special properties of ngRepeat, as seen in the demo
below; and for injecting data via server side scripting. Besides these
few cases, you should use controllers rather than ngInit to initialize
values on a scope.
OK, I can ignore the strange behavior of second confusion.
I have not read the book ng-book, but as far as I know, the statement of your first confusion does not conform with the AngularJS documentation regarding inherited and isolated scopes within a directive.
The statement above is simply not possible, having multiple directives that each have their own isolated scope would produce a $compile:multidir error. Here is a DEMO.
.directive('elem1', function($rootScope) {
return {
restrict: 'A',
scope: {}
};
})
.directive('elem2', function() {
return {
restrict: 'A',
scope: {}
}
});
After scanning the AngularJS documentation, there is no supporting statement that validates the statement:
If multiple directives on an element provide an isolate scope, only
one new scope is applied.
The closest statement I see that is similar to the statement above is the scope: true option definition when creating directives:
true: A new child scope that prototypically inherits from its parent
will be created for the directive's element. If multiple directives on
the same element request a new scope, only one new scope is created.
The new scope rule does not apply for the root of the template since
the root of the template always gets a new scope.
The statement above suggests that when multiple directives with scope: true option (not an isolated scope) resides in a single element, it would create one scope and everything else is a shared scope. DEMO
.directive('elem1', function($rootScope) {
return {
restrict: 'A',
scope: true,
link: function(scope) {
console.log(scope.hello);
}
};
})
.directive('elem2', function() {
return {
restrict: 'A',
scope: true,
link: function(scope) {
scope.hello = 'world';
console.log(scope.hello);
}
}
});
You would notice that both directives logs 'world', this obviously supports the statement above.
If you read more in the $compile scope AngularJS documentation, you would see that:
In general it's possible to apply more than one directive to one
element, but there might be limitations depending on the type of scope
required by the directives. The following points will help explain
these limitations. For simplicity only two directives are taken into
account, but it is also applicable for several directives:
no scope + no scope => Two directives which don't require their own scope will use their parent's scope
child scope + no scope => Both directives will share one single child scope
child scope + child scope => Both directives will share one single child scope
isolated scope + no scope => The isolated directive will use it's own created isolated scope. The other directive will use its parent's scope
isolated scope + child scope => Won't work! Only one scope can be related to one element. Therefore these directives cannot be applied to the same element.
isolated scope + isolated scope => Won't work! Only one scope can be related to one element. Therefore these directives cannot be applied to the same element.
Perhaps you're having problems identifying the difference between prototypically inherted scopes and isolated scopes. You might want to read the $rootScope.Scope $new() method, the isolate parameter definition.
First question:
I think that mean other directives will use the isolate scope as
theirs. is that right?
The answer is an absolute no, in reference towards multiple directives with isolated scope, it would produce the $copile:multidir error.
For your second question:
ng-init has higher priority than custom directive. Why the expression
of ng-init will use the scope of custom directive?
As for directives that do not have isolated scopes or directives that don't prototypically inherit from their parent scope, you can directly associate those directives as having a scope property definition with a falsey scope value:
falsy: No scope will be created for the directive. The directive will
use its parent's scope
If a directive is bound to an element with a scope of its own then it simply uses the scope of that element, otherwise it seeks all the scope instances within the scope chain until it reaches the $rootScope.

bindToController: Object in directives

Directive bindToController can be either boolean or object, the latter is shown here:
myMod.directive('myDirective', {
controller: 'MyDirectiveController',
bindToController: {
name: '#'
}
});
But the fact that it was not documented raises questions. Why bindToController: { ... } feature was made in the first place? Are there useful scenarios for it?
Despite bindToController wasn't primarily intended for that, it is interesting to see how it is utilized now in angular.component as bindings property to fill the gap between 1.5 and 2.0, while scope bindings remain unused.
bindToController was originally just a boolean at inception, but was migrated to allow it to be an object to be more explicit about what items/values you are binding to the controller. With it being a boolean it caused some confusion where this syntax removes that confusion about what you are adding to your controller.
The idea with why this was added was to propagate the usage of the controllerAs syntax to move away from $scope especially with the move towards angular2.
The basis for why this was added was to allow the directive injections/property bindings would now be based upon the controller instance instead of of the scope parameter.
Just stumbled across this PR, it is quite explanatory.
I'm not sure if there is practical benefit in having two different bindings in scope: { ... } and bindToController: { ... }. But it finally brings the bindings to prototypically inherited scope as well:
bindToController: {
text: '#text',
obj: '=obj',
expr: '&expr'
},
scope: true

ng-if in transcluded scope breaks scope inheritance

That fiddle illustrates the issue http://jsfiddle.net/LAqeX/2/
I want to create a directive that wraps a part of the page and hides it. And i would like to use ng-if to remove unnecessary bindings. But some black magic happens.
This is my preferable directive code.
app.directive('withIf', function(){
return {
restrict: 'E',
scope: {
title: '#'
},
transclude: true,
template: '<div>' +
'<p ng-click="visible = !visible">{{title}}</p>' +
'<div ng-if="visible" ng-transclude></div>'+
'</div>',
link: function(scope){
scope.visible = false;
}
}
});
It is supposed to create two scopes:
Directive isolate scope which has two variables - title and visible
Transcluded scope which prototypically inherits from "regular" scope tree.
However, ng-if makes transclued scope somewhat detached from reality and trasncluded scope does not inherit from controller. Please, see the fiddle, it illustrates the issue very clear.
Any ideas what is happening there and how to solve it?
UPDATE
It seems i have figured out reasons why scope chain looks broken. The scope created by ng-if belongs to withIf directive isolate branch. So it never knows that controller's scope even exists. But the question remains the same - how to use ng-if in such case.
ng-if creates a child scope, use $parent to access variables from parent scope. But in your case I would consider using ng-show or ng-hide instead of ng-if (they don't create child scopes)
This bug seems to be fixed in Angular 1.3 - https://github.com/angular/angular.js/pull/7499

What is the default for AngularJS directive scope?

What is the default scope value of an AngularJS directive?
Of course, it is not isolated scope. It is true or false.
I can't find any documentation on what it is.
"Note, by default, directives do not create new scope -- i.e., the default is scope: false"
from Understanding scopes.
Using the scope option in directive you can:
create a child scope prototypically inherited with scope: true
create an isolated scope with scope: {} then you can bind some property to parent scopes with '#', '&', '=' (see this question).
decide to not create a new scope and use parent with scope: false (default).
By default, directives shared the scope of the containing controller.
You can specify scope:true to have a inherited scope and to get a isolated scope you have to do the following
scope:{
something1:"#",
something2:"="
something3:"&"
}

Can I replace scope for isolated scope?

I have directive in which I have scope mentioned as :
return {
restrict: 'AE',
transclude: true,
scope: {
model: '=',
menu: '='
},}
so my question is apart from model and menu variables what all variables are accessible to me? Does Isolated scope inherits parent scope variables ?
If I don't want to use isolated scope then what could be done where I can set these two variables in directives and inherits all variables from parent scope ?
Thank you.
If I understood correctly, what you want to achieve is a new scope inheriting all the parent stuff. That is easy to do.
By default a directive uses the parent scope, but you can do two different things:
scope: {} // Isolated scope
or:
scope: true // new scope inheriting from parent
So with the later, you will have all the parent scope has but you can set new stuff there and the parent won't know. Example:
http://plnkr.co/edit/oL5ALPvkEzkiXSuOSNnE?p=preview
I hope this is what you asked for.
EDIT: I edited the plunker. The idea with new scopes that inherits from others is:
If parent has a primitive like name the child will inherit it, but if you do something like:
childScope.name = ".." you are not modifying the parent name, you are shadowing it, AKA creating a new name that will hide the parent name. this means that the parent will never know if the child modified the name.
foo is something created in the new child scope, that means that the parent will never know.
user is not a primitive, it is an object and when the child modifies its name, it is not shadowing the entire user, it is just modifying its value:
childScope.user.name = "Fox"
This is getting the user reference and modifying its value, is not modifying the entire user, so you are not shadowing it. If you do:
childScope.user = { name: 'Foo' };
Then you're creating an entire user, AKA you're creating a new reference so that will shadow the parent user and the parent-child relationship of that object will end.
This is what we call the dot rule.
If the scope property of the directive definition is set to an { /* object hash */ } an isolated scope is created for your directive. If you set the scope property to true a new scope is generated that inherits from the parent scope.
See this plunker for a demo on directive scope behaviour.
If you don't want to isolate the scope but do want to pass argument to the directive you can use the $observe method of the attributes service.
function linkingFn(scope, elm, attrs, ctrl) {
// get the attribute value
console.log(attrs.ngModel);
// change the attribute
attrs.$set('ngModel', 'new value');
// observe changes to interpolated attribute
attrs.$observe('ngModel', function(value) {
console.log('ngModel has changed value to ' + value);
});
}
Creating a two way binding without an isolated scope can be done with the $parse service.
when you set scope like this, it's an isolated scope, and as its name says, it doesn't inherit any property from parent scope.
However, you can directly define variable on the scope, in the directive link method, like this:
.directive('someDirective', function(){
return {
restrict: 'C',
link: function(scope, element, attrs){
scope.newVariable = "newVariable"
}
}
}
it adds newVariable to your scope.Hope it helps~
More info on directive can be seen here, http://docs.angularjs.org/guide/directive. There is a section with the name Creating a Directive that Manipulates the DOM

Resources