Well, since 'improve this doc' button on AngularJS documentation site doesn't work and discussion is now closed, I'd like to ask a question about 'isolated scope pitfall' paragraph of ngModelController.
<div ng-app="badIsolatedDirective">
<input ng-model="someModel"/>
<div isolate ng-model="someModel"></div>
<div isolate ng-model="$parent.someModel"></div>
</div>
angular.module('badIsolatedDirective', [])
.directive('isolate', function() {
return {
require: 'ngModel',
scope: { },
template: '<input ng-model="innerModel">',
link: function(scope, element, attrs, ngModel) {
scope.$watch('innerModel', function(value) {
console.log(value);
ngModel.$setViewValue(value);
});
}
};
});
I expected to see the third input affecting first one (cause we just isolated second input's scope and have no reference to 'someModel' scope value), btw behavior of this example is just stunning: second input affects first, third doesn't affect anything. So the question is: am I loosing the concept or just don't understand it, or there are mistakes (maybe, not mistakes, but just no connection to the topic) in the example code (well, I changed it on Plunkr to make it work as I expected).
In 1.2.0 timely-delivery there was a major change (here) to how multiple isolate scope directives on the same element work. This change apparently hasn't been reflected in their documentation.
Prior to 1.2.0 all directives on an element shared an isolate scope if any of the directives requested an isolate scope. Therefore in the above example the ngModel directive shared the isolate directive's scope. Which is why we had to reference the parent scope like this- ng-model="$parent.someModel"
That is no longer true in 1.2.0.
In 1.2.0 and beyond the ngModel directive no longer shares scope with isolate. ngModel is now on the parent scope of the isolate directive. Thus we now need ng-model="someModel" instead of ng-model="$parent.someModel"
Here's their description of the change (keeping in mind as you read this that ngModel is a directive):
Make isolate scope truly isolate
Fixes issue with isolate scope leaking all over the place into other directives
on the same element.
Isolate scope is now available only to the isolate directive that requested it
and its template.
A non-isolate directive should not get the isolate scope of an isolate directive
on the same element,instead they will receive the original scope (which is the
parent scope of the newly created isolate scope).
BREAKING CHANGE: Directives without isolate scope do not get the
isolate scope from an isolate directive on the same element. If your
code depends on this behavior (non-isolate directive needs to access
state from within the isolate scope), change the isolate directive to
use scope locals to pass these explicitly.
Before
<input ng-model="$parent.value" ng-isolate>
.directive('ngIsolate', function() { return {
scope: {},
template: '{{value}}' }; });
After
<input ng-model="value" ng-isolate>
.directive('ngIsolate', function() { return {
scope: {value: '=ngModel'},
template: '{{value}} }; });
Here's a version running 1.2.0-rc3 (the last version before this change) which operates like their documentation describes: http://jsfiddle.net/5mKU3/
And immediately after, 1.2.0 stable, we no longer need, or want, the reference to '$parent': http://jsfiddle.net/5mKU3/1/
Related
I'm tring to write a directive that builds an object from its child directive's input and pushes it
to an array provided as a parameter. Something like:
<aggregate-input on="SiteContent.data.objList">
<p aggregate-question>Question text</p>
<input aggregate-answer ng-model="answer" type="text" />
</aggregate-input>
<aggregate-input on="SiteContent.data.objList">
<p aggregate-question>Question text 2</p>
<input aggregate-answer ng-model="answer" type="text" />
</aggregate-input>
I'm looking to collect the data like:
SiteContent.data.objList === [
{question: 'Quesion text', answer: 'user input'},
{question: 'Quesion text 2', answer: 'user input 2'},
];
Here's the plunker with the code.
update 1: #jrsala included scope and bindToController syntax changes
I'm having trouble figuring out the way these directives should communicate. I'm expecting the input
object defined in the link will be isolated in the scope of each directive and pushed to the on object
provided. The result is that the input object is shared among all instances, and only one object gets
ever pushed to the array.
I'm guessing the transcluded scope rules are confusing me, but I really don't see where. Any ideas? Thanks!
First issue: your aggregate-input directive specifies an isolate scope with no bound properties but you still use an on attribute on the element with the directive on it:
<aggregate-input on="SiteContent.data.objList">
but in your JS,
{
restrict: 'E',
scope: {},
controller: aggregateInputController,
controllerAs: 'Aggregate',
bindToController: { on: '=' },
/* */
}
whereas what you need is
{
restrict: 'E',
scope: { on: '=' },
controller: aggregateInputController,
controllerAs: 'Aggregate',
bindToController: true // BOOLEAN REQUIRED HERE, NO OBJECT
}
As per the spec's paragraph on bindToController,
When an isolate scope is used for a component (see above), and controllerAs is used, bindToController: true will allow a component to have its properties bound to the controller, rather than to scope. When the controller is instantiated, the initial values of the isolate scope bindings are already available.
Then you do not need to assign the on property to your controller, it's done for you by Angular (also I did not understand why you did this.on = this.on || [], the this.on || part looks unnecessary to me).
I suppose you can apply that to the rest of the code and that should be a start. I'm going to look for more issues.
edit: Several more issues that I found:
If the scope of siteContent is isolated then the SiteContent controller is not accessible when the directive is compiled and Angular silently fails (like always...) when evaluating SiteContent.data.objList to pass it to the child directives. I fixed that by removing scope: {} from its definition.
It was necessary to move the functionality of aggregateInputLink over to aggregateInputController because, as usual, the child controllers execute before the link function, and since the aggregateQuestion directive makes the call InputCtrl.changeValue('question', elem.text()); in its controller, the scope.input assigned to in the parent directive post-link function did not exist yet.
function aggregateInputController($scope) {
$scope.input = {};
this.on.push($scope.input);
this.changeValue = function changeValue(field, value) {
$scope.input[field] = value;
};
}
As a reminder: controllers are executed in a pre-order fashion and link functions in a post-order fashion during the traversal of the directive tree.
Finally, after that, the SiteContent controller's data did not get rendered property since the collection used to iterate over in ng-repeat was erroneously SiteContent.objList instead of SiteContent.data.objList.
Link to final plunker
I would like to create a directive that has transcluded content that the directive can bind to and modify. The directive has an isolate scope. I imagine it working something like this:
<my-directive bound-item-name="childObj">
<input ng-model="childObj.someField">
</my-directive>
At runtime, I want to use childObj as an alias for an object on my-directive's isolate scope called activeObject. Essentially, you might think of this as similar to the way ng-repeat lets you use a statment like obj as alias in objList and in the transcluded content alias refers to the individual instance.
I can't seem to figure out how I can actually do this... if I change the transluded content to refer to $parent.activeItem it does work the way I intended, but I feel like that's expecting the transcluded content to know too much about how the directive works. It seems like modifying in the compile function might work, except I can't see, in the docs, how I can actually do that with the transcluded content. Forcing the transcluded content to share its scope with the directive would be OK, although I see no evidence that there's some way to do that.
This must be possible, but how?
Fiddling around with this some more, I am able to get it to work by modifying scope.$$childHead[scope.boundItemName] instead of using scope.activeObject in the directive. While this works I'd like to not rely on undocumented internal objects, if possible.
The link function of the directive is given the transclude function as the 5th parameter.
link: function(scope, element, attrs, ctrls, transclude){
// ...
}
This transclude function takes a scope variable that you can create and another function - called "clone linking function" - that places the pre-linked transcluded content in the DOM. The transclude function links against that scope variable that you provided.
Here's how it works.
transclude: true,
scope: {}, // you are free to use whatever scope you need
link: function(scope, element, attrs, ctrls, transclude){
var boundObj = {}; // your object
var alias = attrs.boundItemName;
// let's create an isolate scope for the transcluded content
var newScope = scope.$new(true);
newScope[alias] = boundObj;
transclude(newScope, function(preLinkContent){
element.append(preLinkContent);
});
}
Then, if you used your example:
<my-directive bound-item-name="foo">
<input ng-model="foo.text">
</my-directive>
Then, the transcluded ng-model would write into your internal boundObj's .text property.
Demo
I have been working with isolated scope directives for a little time and a question came in mind watching it's behavior:
Why can't i bind variables that i define inside the directive inherited scope directly to the view?
Let me show an example on this code pen:
http://codepen.io/anon/pen/VLKjrv
When i create a new $scope variable inside the directive controller and i try to bind it on the view, it does not works.
By the other hand, when i bind that variable on a html that comes from the template directive attribute, it does works.
Check out the code:
<body ng-app="isolated-test-app">
<section ng-controller="isolatedTestCtrl">
<article>
<h1>test1</h1>
<div isolated-directive binding-from-attr="test">
<span ng-bind="test"></span>
<span ng-bind="test2"></span>
</div>
<h1>test2</h1>
<div isolated-directive-number-two binding-from-attr="test">
</div>
</article>
</section>
angular.module('isolated-test-app', [])
.controller('isolatedTestCtrl', function isolatedTestCtrl($scope){
$scope.test = 'Binded from parent controller';
})
.directive('isolatedDirective', function isolatedDirective(){
var directive = {
scope: {
bindingFromAttr: '=',
},
controller: function directiveController($scope){
$scope.test2 = 'Binded from directive controller!';
},
};
return directive;
})
.directive('isolatedDirectiveNumberTwo', function isolatedDirective2(){
var directive = {
scope: {
bindingFromAttr: '=',
},
template:'<span ng-bind="bindingFromAttr"></span>\
<span ng-bind="test2"></span>',
controller: function directiveController($scope){
$scope.test2 = 'Binded from directive controller!';
},
};
return directive;
})
test1
Binded from parent controller
test2
Binded from parent controller
Binded from directive controller!
I was expecting the result of test2 on test1.
Why does that happens?
There is a difference between directive template and the directive's element's contents with regards to what scope applies.
In isolate scope (scope: {}) directives, the isolate scope applies to the template, but not to the contents. The contents have the same scope as the directive's parent. Also, note, that the contents would be replaced by the template, if the template is defined. To use the contents in addition to the template requires "transcluding" (transclude: true) (this is, however, outside of scope for this answer).
If you are confused, you could always check $scope.$id to see which scope applies:
<div>parent scope: {{$id}} (will be 2)</div>
<isolate-scope>
contents of isolate scope directive: {{$id}} (will also be 2)
</isolate-scope>
<isolate-scope-with-template>
contents will be replaced with the template
</isolate-scope-with-template>
.directive("isolateScopeWithTemplate", function(){
return {
scope: {},
template: "template: {{$id}} (will NOT be 2)"
}
})
(of course, the actual $id could be different)
In child scope (scope: true) directives, the scope that applies to the content is actually the same that would have applied to the template (same here - the template would replace the contents if it exists, unless you transclude).
Now, to answer your question:
The first <span ng-bind="test2"></span> binds to a non-existent $scope.test2 in the parent scope and so it is empty.
But the <span ng-bind="test2"></span> in the template of isolatedDirectiveNumberTwo binds to the isolate scope of that directive, which defines $scope.test2 = 'Binded from directive controller!'.
This is my guess base on experiment in http://codepen.io/anon/pen/MwjjBw
so for test 1, the directive scope doesnt have test/test2 since the dom object is belong to controller. Hence in order to update it you have to use
$scope.$parent.test2 = "" ;
and for test 2, as the template is created as part of directive hence the dom object is belong to directive and also accessible by controller ( I guess $compile adding this into controller scope/watch).
You can also see that test1 doesnt have any watcher as there is no binding happen.
Now i get the whole picture, as New Dev's answer stated on his answer
In isolate scope (scope: {}) directives, the isolate scope applies to the template, but not to the contents. The contents have the same scope as the directive's parent.
So i have learned that there is a difference between directive contents and template and how the scope is inherited on isolated scopes.
For my application setting the scope to true solved entirely my problem.
Also, kwan245's solution is a real good work-around of this issue.
Both answers cleared my mind, many thanks to New Dev and kwan245 :)
I am trying to understand $parse, based on the documentation. But I am having trouble to get my test code working. Am I using $parse service the right way?
The main part of the code is:
app.directive('try', function($parse) {
return {
restrict: 'E',
scope: {
sayHello: "&hello"
},
transclude: true,
template: "<div style='background:gray;color:white'>Hello I am try: <span ng-transclude></span><div>",
link: function($scope, $elem, $attr) {
var getter = $parse($attr.sayHello);
// var setter = getter.assign;
$elem.on('click', function() {
getter($scope);
$scope.$apply();
});
}
};
});
See my code at: http://plnkr.co/edit/lwV5sHGoCf2HtQa3DaVI
I haven't used the $parse method, but this code achives what you are looking for:
http://plnkr.co/edit/AVvxLR4RcmWhLo8eqYyd?p=preview
As far as I can tell, the $parse service is intended to be used outside of an isolate scope.
When you have an isolate scope, like in your directive, you can obtain a reference to the parent scope's function using the 'sayHello': '&' as proposed in Shai's answer. The $parse service might still work as expected even with an isolate scope, if you are able to pass in the parent scope instead of the directive's scope when calling getter($scope), but I haven't tested that.
Edit: This is indeed the case - using getter($scope.$parent) works fine. When an isolate scope is used in your directive, the $scope variable no longer refers to the correct context for the getter function returned by the $parse service. Access the correct one by using $scope.$parent.
However, if you are avoiding an isolate scope, your approach works well. Try removing the scope: { ... } section out of your directive definition entirely and you'll see it works fine. This is handy if you are creating a directive for event binding that might be applied to an element in conjunction with another directive that has an isolate scope, say a dragenter directive (which isn't provided by Angular). You couldn't use Shai's method in that case, since the isolate scopes would collide and you'd get an error, but you could use the $parse service.
Here's an updated plunker with the scope removed from the directive definition: http://plnkr.co/edit/6jIjc8lAK9yjYnwDuHYZ
I have created a directive to understand transclusion and isolated scope.
Here is the HTML:
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<div>
<input type="text" ng-model="name" />
<input type="number" ng-model="friendCount" />
</div>
<my-friends name="{{name}}">
<p>Hi, I'm {{name}}, and I have {{friendCount}} friends</p>
</my-friends>
</div>
</div>
Directive:
var app = angular.module('myApp', []);
app.controller('MyCtrl', function ($scope) {
$scope.name = "John Doe";
$scope.friendCount = 3;
});
app.directive('myFriends', function () {
return {
restrict: 'E',
replace: true,
template: '<div>' +
' <h3> Isolated Scope: {{name}}</h3>' +
' <div ng-transclude></div>' +
'</div>',
transclude: true,
scope: {
name: '#'
},
link: function (scope, element, attrs) {
scope.name = "Anderson";
}
}
});
Open this fiddle: JsFiddle
I have two questions:
Why does the {{name}} in directive template says "John Doe" instead of "Anderson"? I expected it to be Anderson, since name property is prototypically inherited and as soon as write to it in the link function, it should lose that ancestral connection.
It seems to be transcluding correctly but why does it throw the error Error: [ngTransclude:orphan] in the dev tools console? Could it be the angular version I am using?
Any help is greatly appreciated.
Here is the fiddle: JsFiddle
UPDATE:
The transclusion error was due to loading angular twice by mistake.
When using the # binding you can overwrite the name inside directive after the initial digest cycle has completed, for example in an click handler or something. However, it will be overwritten as soon as you change the parent scope's name property.
The issue about # binding is my misunderstanding of isolated scopes. It is working the way it is supposed to in the example. Isolated scope does not prototypically inherit from parent. And the meaning of # binding, also referred to as read-only access or one-way binding, is that it will not let you update/write to parent scope.
UPDATED FIDDLE
1) With # binding, at the digest phase, angular will reevaluate the expression and set the value back to the current value of your controller which will overwrite your value set in the link function.
2) You have 2 versions of angular loaded in the fiddle. The angular loaded the second time walks the DOM again and tries to compile again the already compiled DOM. For more information, take a look at my answer to another question Illegal use of ngTransclude directive in the template
DEMO
The AngularJS documentation is in such a state of flux that I cannot find the page relevant to the different types of isolated scope bindings so I apologize for any inaccuracies.
How to make it work
scope: {
name: '=' //formerly #
},
IIRC the # binding takes the value passed in as a string literal. I am not positive why this means the link function assignment doesn't overwrite it, if I could find the section in the docs then I'd link to it.
The = binding binds to the value of a scope property and updates both the isolate scope and the assigning scope when updated.
<my-friends name="name"> //formerly name="{{name}}"
Using the = binding means that a scope property needs to be passed instead of a string value.
Again, sorry for any incorrect or vague information. I'll update the answer if I can find the dang documentation.