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
Related
I have a tree directive and I want when user clicked on some thing happens..it should be implement with who wants to use this tree. for example node's text changes.
index.html
<div ng-controller='TestController'>
<tree model="treedata" on-node-clicked="changeNodeText($node)"></tree>
</div>
Directive
function treeDirectiveFactory() {
return {
restrict: 'E',
scope: {
model: '=model',
collapseIcon: '=',//IIconProvider//TODO:
expandIcon: '=',//IIconProvider//TODO:
onNodeClicked:'&',//param:$node//TODO:
isExpanded:'#'
},
templateUrl: '/_Core/DirectiveCore/Tree/TreeTemplate.html',
controller: 'TreeController',
controllerAs: 'c'
}
};
part of template
<a href={{node.link()}} class="node-link" ng-click="c.onNodeClicked(node)"
ng-class="{'margin-no-child':node['__$extension'].isLeaf(node)}">
<span ng-bind-html="node.iconProvider().htmlPath()"></span>
{{node.text()}}
</a>
when user click on node, ng-click="c.onNodeClicked(node)" will call and the node which is clicked is passed to onNodeClicked function. below is the implementation of this function in controller as c of tree directive
onNodeClicked(node: Core.INode) {//TODO:
if (this.scope["onNodeClicked"]) {
this.scope["onNodeClicked"] = ({$node:node});
}
}
I want to tell the function that you have an argument named $node and set the value of $node. then I want to change the $node text in outer controller TestController in index.html...this is the changeNodeText function in TestContoller
f($node: Core.TestNode) {
if($node !== undefined)
$node.t = "Clicked!";
}
but nothing changes, actually changeNodeText function never called. I know there is something wrong but unfortunately I can not figure it out. any help would be appreciated.
Answer for #Parud0kht
Instead of setting the function, invoke it.
onNodeClicked(node: Core.INode) {//TODO:
if (this.scope["onNodeClicked"]) {
//Do THIS
this.scope["onNodeClicked"]({$node:node});
//Not THIS
//this.scope["onNodeClicked"] = ({$node:node});
}
}
Answer for Other Readers
This example does it with components, but the same principle applies to directives.
angular.module('app.dashboard')
.component('dashboardComponent', {
templateUrl: 'app/dashboard/directives/dashboard-container.html',
controller: DashboardComponent,
controllerAs: 'DashboardCtrl',
bindings: {
onTileChange: "&"
}
})t
To communicate event data from a component to a parent controller:
Instantiate the dashboard-component with:
<dashboard-component on-tile-change="HomeCtrl.onTileChange($tile)">
</dashboard-component>
In the component controller invoke the function with locals:
this.onTileChange({$tile: tile});
The convention for injected locals is to name them with a $ prefix to differentiate them from variables on parent scope.
From the Docs:
& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given <my-component my-attr="count = count + value"> and the isolate scope definition scope: { localFn:'&myAttr' }, the isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression to the parent scope. This can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment($amount) then we can specify the amount value by calling the localFn as localFn({$amount: 22}).
-- AngularJS Comprehensive Directive API Reference
someone answered my question completely right, but unfortunately the answer was removed so quickly, I was so lucky to saw it but I could not accept it as an answer..thanks to anonymous user for his or her great answer. hope he\she will see this answer and contact me.
I should invoke the function in onNodeClicked not set it..how crazy I am !! :))) here is the answer which is working well.
onNodeClicked(node: Core.INode) {
if (this.scope["onNodeClicked"]) {
//invoking, function should be called.
this.scope["onNodeClicked"]({ $node: node });
//this is wrong and it will set an object
//this.scope["onNodeClicked"] = ({ $node: node });
}
}
I am passing a custom scope object to the $compile and creating a custom template. If I apply a directive on the elements inside the template, scope that is changing is the one that is passed to the $compile, and that's really what I wanted.
However, I just thought that it might be good to also have a controller on some elements inside the template,
<div ng-controller="controllerName" >
</div>
but ng-controller doesn't set data on the passed scope but creates its own and uses that one. Is there a way to make ngController to use existing scope and not create a new one ?
We create our controllers and wrap them in factories to make them accessible. We apply or controllers through directives (also going away). This gives you a controller that is scoped to the directive, which has better control for scope, this works for us as the directives where we do this for are usually components.
I don't know if this will be an option given the road you are down now. I would suggest trying to stop using ng-controller. You may want to look at angular 2 now just to keep it in mind as a migration path, it is coming in the fairly near future. They have removed ng-controller, a lot of what they are doing in angular 2 can be done now.
This is a good resource on why these things are a bad idea
https://www.youtube.com/watch?v=gNmWybAyBHI&t=9m10s
If you look at the source code for ng-controller, you will see it is very simple:
var ngControllerDirective = [function() {
return {
restrict: 'A',
scope: true,
controller: '#',
priority: 500
};
}];
You can actually create an almost identical alternate directive that just defines scope: false (or omits the scope key altogether, same thing):
app.directive('controllerNoScope', function () {
return {
restrict: 'A',
scope: false,
controller: '#',
priority: 500 // same as ng-controller
}
});
(You may want to give it a better name).
See this Plunkr for a demo that shows the scope has the same $id as the outer one, meaning it is the same scope.
I have a directive where a list(array) of items is passed in through the scope of the controller into the scope of the directive whereby the template of the directive then has access to the items.
I would like to have it so that the list of items is passed to the directive (where it is then used within the link function) and then not directly accessible through the directive's template.
i.e. if we had the following directive:
directive('itemList', function(){
return {
scope: {
items: '='
}
link: function(scope, elem, attrs){
var item-list = scope.items;
// ... want to manipulate first ...
}
}
})
the variable scope.items is now available to any template that the directive uses. Whereas I don't want that to be the case and would like to pass in something to the directive without making it known to the scope. Is this possible?
Since the directive scope pretty much by definition is the scope used by the directive's template, I don't see any obvious way of doing this in a strict information hiding way. But, why not just use a different name for the passed scope variable and what the template binds to? For example, if you said scope: { myPrivatePassedItems: '=items' }, you can now work with scope.myPrivatePassedItems as much as needed before setting it as scope.items. With this approach, the HTML in both the usage of the directive and the directive's template just sees "items", but internally your directive has its own "private" variable.
I should add that the above is the simple change needed for the one-way data flow from the consumer to the directive template. If you also need to update the original items array, you will also want to add a scope.$watch on the scope.items array after you have done your initial setup. You would then need to carry those changes (possibly with modifications) back to scope.myPrivatePassedItems in order to update the consumer's array.
You can use the $parse service to retrieve the value without using scope: {}, but you will lose the 2 way data binding that you inherently get from using scope: { items: '=' }. As mentioned by Dana Cartwright, if you need 2 way binding, you have to set this up manually with watches.
directive('itemList', function($parse){
return {
link: function(scope, elem, attrs){
var itemList = $parse(attrs['items'])(scope);
// do stuff with itemList
// ...
// then expose it
scope.itemList = itemList;
}
};
});
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.
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/