angularjs directive with ng-repeat not respecting = scope binding - angularjs

I have created a directive with an isolated scope with two properties. One of them set to data-binding with the equal sign. If I manually insert the directive several times the html document, changes to the values are reflected, as expected, on the scope in the controller. But if I insert the elements with a repeater (ng-repeat) the connection to the scope on the controller no longer works. Any idea why?
The directive looks like this:
myApp.directive("phone", function(){
return{
restrict: "E",
scope:{
number:"#",
dirname:"="
},
template: '<div class="panel"> <input type="text" ng-model="dirname"><br>Number:{{number}} {{dirname}}</div> '
}
});

I'll guess (since you didn't provide any HTML or model data) that you have an array of dirnames, so inside the ng-repeat, you are trying to bind the ng-model to a primitive. Since each iteration of ng-repeat creates its own child scope, when you first type into a textbox, a dirname primitive property will be created on the child scope. (This is how JavaScript prototypal inheritance works.)
The fix is to use an object rather than a primitive.
$scope.names = [ {name: 'Superhero'}, {name: 'Julio'} ];
<li ng-repeat="nameObj in names">
<phone number="123" dirname="nameObj.name"></phone>
</li>
Fiddle.

Related

Two way binding from the link function

Can someone tell me why I am not able to two way bind from the link function?
Please refer to this plunk: http://plnkr.co/edit/RI1ztP?p=preview
The below watch successfully adds the collection to attrs.ngModel but I dont see it reflecting in the parent controller
scope.$watchCollection("selectedItems",function(collection){
attrs.ngModel = [];
for(var i=0;i<collection.length;i++){
attrs.ngModel.push(collection[i]);
}
console.log("ngModel",attrs.ngModel);
});
Cant see the collection over here (selectedUsers):
<body ng-controller="mainCtrl">
<div multi-select-search-box ng-model="selectedUsers" label="name" my-options="state in states"></div>
{{selectedUsers}}
If you look at the above html, I am binding the selectedUsers array to ng-model. In my link function, i add the selected users to attrs.ngModel array. When I look at the console, the selectedUsers are added to attrs.ngModel but the array isn't reflected back on the html {{selectedUsers}}
The data bound to the ng-model of your multi-select-search-box is $scope.selectedUsers.
Therefore to register a change in the DOM you have to update that variable rather than ng-model.
scope.$watchCollection("selectedItems",function(collection){
for(var i=0;i<collection.length;i++){
scope.myNgModelVar.push(collection[i]);
}
});
Since ng-model is a string that gets $parse()/$eval() called on it to evaluate it as an expression, updating that ng-model value won't do you any good.
EDIT:
After some clarification it appears that this is a custom directive designed to be reusable. So therefore we do not want to stick variables from your controller inside the directive. Instead, you should bind a directive attribute to your directives scope.
// Directive Def. Object:
return {
restrict: "AE",
scope: {
myNgModelVar: "=",
bindModel: "=ngModel" //This is the alternate method aliasing ngModel var with a scope var.
},
template: "<input ng-model='myNgModelVar' />"
};
Although you could use ngModel by using an alias scope: {bindModel:'=ngModel'}, this gives you an isolated scope variable that you bind to ngModel instead. Therefore keeping your directive reusable.
The solution was to require the ng-model controller and sync changes using the viewValue array:
scope.$watchCollection("selectedItems",function(collection){
ctrl.$viewValue.splice(0,ctrl.$viewValue.length);
for(var i=0;i<collection.length;i++){
ctrl.$viewValue.push(collection[i]);
}
});
and
require: 'ngModel'

Angularjs assign a ngmodel of element when template is loaded

I have the following directive:
app.directive("mydirect", function () {
return {
restrict: "E",
templateUrl: "mytemplate.html",
}
});
The template from mytemplate.html is:
<input ng-model="name{{comment.ID}}" ng-init="name{{comment.ID}}={{comment.Name}}" />
I load the template several times and for each time I want to change the variable assigned as the ng-model, for example ng-model="name88" (for comment.ID == 88).
But all the loaded templates have the same value.
But when I change comment.ID, all inserted templates become the last ID changed.
First of all, you cannot put expressions, like name{{comment.ID}} in ng-model - it needs to be assigned to a variable.
So, let's change the template to:
<input ng-model="comment.ID" ng-init="comment.ID = comment.Name">
It's not entirely clear what you mean by "load the template". If you mean that you create a mydirect directive for each comment object, then you are probably doing this (or at least, you should be) with something like ng-repeat:
<div ng-repeat = "comment in comments">
<mydirect></mydirect>
</div>
This is convenient - comment is both the variable used in the ng-repeat, and the variable used for the directive's template. But this is not too reusable. What if you wanted to change the structure of the comment object? And what if you wanted to place multiple directive's side-by-side, without the child scope created for each iteration of ng-repeat and assign a different comment object to each?
For this, you should use an isolate scope for the directive. You should read more about it here, but in the nutshell, the way it works is that it allows you specify an internal variable that would be used in the template and bind it to whatever variable assigned to some attribute of the element the directive is declared on.
This is done like so:
app.directive("mydirect", function () {
return {
restrict: "E",
scope: {
// this maps the attribute `src` to `$scope.model` within the directive
model: "=src"
},
templateUrl: '<input ng-model="model.ID">',
}
});
And, let's say that you have:
$scope.comment1 = {ID: "123"};
$scope.comment2 = {ID: "545"};
Then you could use it like so:
<mydirect src="comment1"></mydirect>
<mydirect src="comment2"></mydirect>
Alternatively, if you have an array of comments, whether you create them statically or load from a service call, you could just do this:
<div ng-repeat = "comment in comments">
<mydirect src="comment"></mydirect>
</div>

Angular directive: using ng-model within isolate scope

I'm having trouble working out how I can define a custom directive that both:
Uses isolate scope, and
Uses the ng-model directive in a new scope within in its template.
Here's an example:
HTML:
<body ng-app="app">
<div ng-controller="ctrl">
<dir model="foo.bar"></dir>
Outside directive: {{foo.bar}}
</div>
</body>
JS:
var app = angular.module('app',[])
.controller('ctrl', function($scope){
$scope.foo = { bar: 'baz' };
})
.directive('dir', function(){
return {
restrict: 'E',
scope: {
model: '='
},
template: '<div ng-if="true"><input type="text" ng-model="model" /><br/></div>'
}
});
The desired behaviour here is that the input's value is bound to the outer scope's foo.bar property, via the the directive's (isolate) scope model property. That doesn't happen, because the ng-if directive on the template's enclosing div creates a new scope, so it's that scope's model that gets updated, not the directive's scope's.
Ordinarily you solve these ng-model issues by making sure there's a dot in the expression, but I can't see any way to do that here. I wondered if I might be able to use something like this for my directive:
{
restrict: 'E',
scope: {
model: {
value: '=model'
}
},
template: '<div ng-if="true"><input type="text" ng-model="model.value" /><br/></div>'
}
but that doesn't work...
Plunker
You are right - ng-if creates a child scope which is causing a problem when text is entered in the input text field. It creates a shadow property named 'model' in child scope which is a copy of the parent scope variable with the same name - effectively breaking the two-way model binding.
The fix for this is simple. In your template, specify the $parent prefix:
template: '<div ng-if="true">
<input type="text" ng-model="$parent.model" /><br/>
</div>'
This ensures that it will resolve 'model' from the $parent scope, which you've already setup for two-way model binding through the isolated scope.
In the end, the '.' in ng-model saves the day. I find it useful to think about anything left of the dot as a way for Angular to resolve the property through scope inheritance. Without the dot, resolving the property only becomes an issue when we're assigning scope variables (otherwise, lookups are fine, including read-only {{model}} binding expressions).
ng-if creates an additional prototypally inheriting scope, so ng-model="model" binds to the inherited property of the new scope and not to the 2-way binded property of the directive scope.
Change it to ng-show and it will work.
You can use a small Firebug extension i've written to inspect angular scopes.

Angular Directive's template binding doesn't update

I have a directive set up here http://jsfiddle.net/screenm0nkey/8Cw4z/3 which has two bindings to the same scope property, but for some reason the binding in the directive's template property doesn't update when the model changes (after typing in the input).
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count">
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
}
};
});
But if I move the input position in the markup it works and both bindings update.
<input type="text" ng-model="count">
<test>
<h3>Inner {{count}}</h3>
</test>
http://jsfiddle.net/screenm0nkey/dCvZk/3
Can anyone explain why the position of the input containing the binding, would have an affect the bindings. I assumed that during the digest loop the watchers for both binding would be updated regardless of the position of the markup.
Many thanks
To me, this seems purely to be a scope issue. Lets take a look at the markup that is generated by both:
Not working:
<body ng-app="App" class="ng-scope">
<h1 class="ng-binding">Outer1 <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner 1</h3>
<input type="text" ng-model="count" class="ng-scope ng-pristine ng-valid">
</div>
</h1>
</body>
Working:
<body ng-app="App" class="ng-scope">
<input type="text" ng-model="count" class="ng-valid ng-dirty">
<h1 class="ng-binding">Outer <div ng-transclude="">
<h3 class="ng-scope ng-binding">Inner </h3>
</div>
</h1>
</body>
The ng-scope class is a useful marker for where Angular is declaring a new scope.
You can see by the markup that the in the working example both the count properties are enclosed in the scope that is attached to body. So, in this case, the directive scope is a child of the body scope (and therefore has access to it).
However, In the example that is not working, the Outer1 property is sitting outside of the scope that the input is in.
The Angular Scope documentation covers this well. The scopes are arranged in a hierarchy with child scopes having access to parent scopes (but not the other way around):
The application can have multiple scopes, because some directives
create new child scopes (refer to directive documentation to see which
directives create new scopes). When new scopes are created, they are
added as children of their parent scope. This creates a tree structure
which parallels the DOM where they're attached
Long story short - as others have said, this is a scope issue. Using the "ng-transclude" directive creates a new scope. When a new scope is created values from the old scope will be accessible in the new scope (hence the first replace) but after that only objects that are shared between the old/new scope will be updated. That is why using an object would work, but using a value will not.
In your case placing the input field inside of the ng-transclude causes this to only edit the value in that scope, not the value in the parent scope (which is where the count for the "test" directive is pulled from).
Incidentally, this can be an issue with repeaters (ng-repeat) as well as other directives. Its best to use a tool such as "Batarang" in order to find issues such as this. It allows you to look at what is in each scope and determine why the screen isn't showing the "correct" data. Hope that helps explain further!
Add ng-change to input , it should work. The problem is that controller into directive doesn't know about count change.
JS
var App = angular.module('App', []);
App.directive('test', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer {{this.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.count = 1;
$scope.onChange = function(count){
$scope.count = count;
}
}
};
});
HTML
<test>
<h3>Inner {{count}}</h3>
<input type="text" ng-model="count" ng-change="onChange(count)">
</test>
Demo Fiddle
The order matters because of the difference between creating a property on the scope versus actually using an object bound to the scope (especially when a transclude creates a new child scopr). Best practice is to use an object on the scope and bind properties to that object when scope issues can come into play with directives and transcludes.
If you change your code to this, it will work as you were expecting and order does not matter. Notice that I am creating a scope object and placing the count as a property on that object.
<test>
<h3>Inner {{data.count}}</h3>
<input type="text" ng-model="data.count"/>
</test>
var App = angular.module('App', []);
App.directive('test', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: "<h1>Outer{{data.count}} <div ng-transclude></div></h1>",
controller: function ($scope) {
$scope.data = {};
$scope.data.count = 1;
}
};
});
This is a great tutorial on this subject. Props to EggHead. https://egghead.io/lessons/angularjs-the-dot
It's a scoping issue.
$scope.count = 1; adds the property count to the scope that <test> is in. Let's call it parent scope.
ng-transclude creates a new scope, let's call it child scope. When <h3>Inner {{count}}</h3> is evaluated, the child scope has no property count so it's read from the parent scope.
<input type="text" ng-model="count"> binds the value of the input to the property count in the child scope. As soon as you enter something the property will be created if it's not there yet. From this point on <h3>Inner {{count}}</h3> gets its value from the child scope.
Scopes in angular are simple JavaScript objects and are connected to their parents via prototypes. So before you enter something the child scope looks something like
{
prototype: { // = parent scope
count: 1
}
}
When you change the value to, say, 5, the scope looks something like
{
count: 5,
prototype: { // = parent scope
count: 1
}
}
Because data binding does something like scope.count = 5.
Here's a work around
Change $scope.count to
$scope.helper = {
count: 1
}
and refactor the rest.
Check this video out for an explanation.
It seems that we cannot override this since ngTransclude will use $transclude function directly.
See: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngTransclude.js
and: http://docs.angularjs.org/api/ng.$compile
transcludeFn - A transclude linking function pre-bound to the correct transclusion scope. The scope can be overridden by an optional first argument. This is the same as the $transclude parameter of directive controllers. function([scope], cloneLinkingFn).

Bind ngInclude to different models

Is it possible to specify model for ngInclude so that any changes done inside the included template are reflected on the specified model. For instance:
I have a model such as :
$scope.member = {
name: "Member1",
children:[
{
name:"Child1"
},
{
name:"Child2"
}
]
};
and the template:
<script type="text/ng-template" id="name.html">
<input type="text" ng-model="member.name"/>
</script>
Now is it possible to pass ngInclude either "member" or any child and get their respective name properties modified? I tried passing ng-model to the ng-template but it doesn't work. I tried to dynamically load the scope with the intended model but if the model gets delete, I am left with an orphan template. Following is the jsfiddle code:
http://jsfiddle.net/vaibhavgupta007/p7E5K/
I wish to reuse the template rather than duplicating the same template for different models. I have refered to this question:
How to specify model to a ngInclude directive in AngularJS?
But here models are not getting deleted.
Edit
I have not grasped the concepts of creating custom directives till now. But will creating a new directive in conjuction with ng-include help?
The answer of your last question is: yes. In a directive, you define also a template and a scope. The content of the scope is completely in your hands.
See here: http://jsfiddle.net/vgWQG/1/
Usage:
Member: <member model="member"></member>
<ul>
<li ng-repeat="child in member.children">
Child {{$index}}: <member model="child"></member>
</li>
</ul>
The directive:
app.directive('member', function(){
return {
template : '<input type="text" ng-model="member.name"/>',
replace : true,
restrict: 'E',
scope : {
'member' : '=model'
},
link: function(scope, element, attr) {}
};
});
I've moved the template in an inline variant because I could not getting the template cache getting to work in jsfiddle. In a real world, a templateUrl: 'name.html' should be fine.
This is what you want?

Resources