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.
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 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'm somewhat new to AngularJs, so forgive me if this is a newb question, but I've looked around a bit and haven't been able to figure this out.
I'm trying to send an attribute of an object into a directive and I'm not quite sure why this isn't working.
I've got a scope variable that's an object, something like:
$scope.player = {name:"", hitpoints:10};
In my HTML, I'm attempting to bind that to a directive:
<span accelerate target="player.hitpoints" increment="-1">Take Damage</span>
In my directive, I'm attempting to modify player.hitpoints like this:
scope[attrs.target] += attrs.increment;
When I trace it out, scope[attrs.target] is undefined, even though attrs.target is "player.hitpoints." When I use target="player", that traces out just fine but I don't want to have to manipulate the .hitpoints property explicitly in the directive.
Edit: I've made a jsfiddle to illustrate what I'm trying to do: http://jsfiddle.net/csafo41x/
There is a way to share scope between your controller and directive. Here is very good post by Dan Wahlin on scope sharing in Directive - http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-2-isolate-scope
There are 3 ways to do so
# Used to pass a string value into the directive
= Used to create a two-way binding to an object that is passed into the directive
& Allows an external function to be passed into the directive and invoked
Just a very basic example on how the above mentioned scope are to be used
angular.module('directivesModule').directive('myIsolatedScopeWithModel', function () {
return {
scope: {
customer: '=' //Two-way data binding
},
template: '<ul><li ng-repeat="prop in customer">{{ prop }}</li></ul>'
};
});
There are a number of things going on here:
#1 - scope
Once you define your isolated scope (along the lines of #Yasser's answer), then you don't need to deal with attrs - just use scope.target.
#2 - template
Something actually needs to handle the click event. In your fiddle there is just <span class="btn"...>. You need ng-click somewhere. In your case, you probably want the directive to handle the click. So modify the directive's template and define the click handler in the directive's scope:
...
template: "<button class='btn' ng-click='click()'>Button</button>",
link: function(scope, element, attrs)
{
scope.click = function(){
scope.target += parseInt(attrs.increment);
}
}
...
#3 - transclude
Now, you need to get the contents of the directive to be the contents of the button within your directive's template. You can use transclude parameter with ng-transclude - for location, for that. So, the template above is modified to something like the following:
...
template: "<button class='btn' ng-click='click()'><div ng-transclude</div></button>",
transclude: true,
...
Here's your modified fiddle
I have an observation directive which render observation.html
observation.js
angular.module('bahmni.clinical')
.directive('observation', function () {
var controller = function ($scope) {
console.log($scope.observation);
};
return {
restrict: 'E',
controller: controller,
scope: {
observation: "="
},
templateUrl: "views/observation.html"
};
});
I call observation directive from the observation.html. This will be done recursively.
observation.html
<fieldset>
<div class="form-field"
ng-class="{'is-abnormal': observation.abnormal, 'is-text': isText(observation)}">
<span class="field-attribute"><label>{{observation.concept.shortName || observation.concept.name}}</label></span>
<span class="value-text-only" ng-if="!observation.groupMembers">{{observation.getDisplayValue()}}</span>
<span class="label-add-on" ng-hide="!observation.unit"> {{observation.concept.units}}</span>
<div class="footer-note fr">
<span class="value-text-only time">{{observation.observationDateTime | date :'hh:mm a'}}</span>
</div>
</div>
</fieldset>
<div ng-repeat="observationMember in observation.groupMembers">
<observation observation="observationMember"></observation>
</div>
I call this for first time from someother directive.
someother.js
<observation observation="observation"></observation>
If i refresh the browser, The tab will be irresponsive. Don't know what is happening. Not able to debug because of the irresponsive tab.
I would really appretiate your answer.
ng-include fixed it.
used the following line
template: '<ng-include src="\'views/observation.html\'" />'
instead of -
templateUrl: "views/observation.html"
It sounds like an endless loop! I think your problem is how the defined local scope variable is defined. observationhas an two way databinding. So you will overwrite it with every recursive call. Try this to avoid the endless loop
scope: {
observation: "&"
},
This will create an local directive scope which are not affected from the parent scope.
Keep in mind there will be three ways to define local scope properties you can pass:
# Used to pass a string value into the directive
= Used to create a two-way binding to an object that is passed into the directive
& Allows an external function to be passed into the directive and
invoked
UPDATE 2
Your main problem seems that your variable observation will be overwritten on each recursive call. So you have two options:
Option 1: You use # and serialize your object with JSON.stringify(observation). This string can you provide via string interpolation to your directive. <observation observation="{{observation}}"></observation>
Option 2: You use & and pass the object to an helper function, which will be create and return an clone from your object.
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/