directive's scope inside ng-repeat angularjs - angularjs

I'm trying to understand directive's scope inside ng-repeat, still wondering if it comes from ng-repeat but it looks like.
Here is my code
directive.js
myApp.directive('dirSample', function () {
return {
template: '<input type="text" ng-model="name" />',
replace: true,
restrict: 'AE'
}
});
mainController.js
angular.controller('mainController',function($scope){
$scope.name = 'name'
});
index.htm
<div ng-repeat="i in [1, 2, 3, 4]">
<dir-sample></dir-sample>
</div>
<dir-sample></dir-sample>
<dir-sample></dir-sample>
When i make a change in one of the last two directives (which are not inside ng-repeat) it works well, changes on one are reflected on the other.
Problem :
1 - if i change an input value of a directive generated by ng-repeat , changes are not reflected anywhere else.
2 - if i change value of input on one of the two last directives , the directives inside ng-repeat change too, but if touch ( change input value ) of any directive , changes will not be reflected on that directive but will keep being reflected on the other directives.
Can someone please explain why the scope has that behavior ?
Thanks.

Binding primitives is tricky, as is explained here: Understanding scopes. It has to with how Javascript works. Your 'name' variable will get shadowed once it is altered within the ng-repeat block. The suggested fix (from the link above):
This issue with primitives can be easily avoided by following the
"best practice" of always have a '.' in your ng-models
They also provide a link to a video explaining exactly this problem: AngularJS MTV Meetup
So a fix looks like this:
app.controller('mainController',function($scope){
$scope.attr= {}
$scope.attr.name = 'name'
});
app.directive('dirSample', function () {
return {
template: '<input type="text" ng-model="attr.name" />',
replace: true,
restrict: 'AE'
}
});

Related

Passing a model to a custom directive - clearing a text input

What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!

ng-repeat render order reversed by ng-if

I've just come across this and am wondering if it's a bug or expected behaviour? This is just a small example to show the issue. The code below is used in both examples:
<body ng-app="app" ng-controller="AppCtrl">
<item-directive ng-repeat="item in ::items" item="item"></item-directive>
</body>
angular
.module('app', [])
.controller('AppCtrl', AppCtrl)
.directive('itemDirective', itemDirective)
.factory('model', model);
function AppCtrl($scope, model) {
$scope.items = model.getItems();
}
function itemDirective() {
return {
restrict: 'E',
replace: true,
scope: {
item: '='
},
templateUrl: 'item-directive.html'
};
}
function model() {
return {
getItems: getItems
}
function getItems() {
return [
{
type: 'test',
title: 'test 1'
},
{
type: 'test',
title: 'test 2'
},
{
type: 'test',
title: 'test 3'
}
];
}
}
The first example has this item-directive.html which gets rendered in the correct order as expected
<div>
<span>{{::item.title}}</span>
</div>
Plunkr without ng-if
But the second example - which has the below item-directive.html - incorporates an ng-if, which is causing the list to get rendered in reverse order?
<div ng-if="item.type == 'test'">
<span>{{::item.title}}</span>
</div>
Plunkr with ng-if
-------- UPDATE ----------
I've just noticed (which relates to the issue noted in #squiroid's answer) that the isolate scope isn't actually working in this example. It appears to be, but item is being made available to the item-directive scope (or rather the scope it ends up with) by the ng-repeat, not the isolate scope. If you try to set any other values on the isolate scope, even though they show up on the scope passed to the directive's link and controller functions (as can be seen in the console output for the plnkr), they're not available to the template. Unless you remove replace.
Plunkr showing broken isolate scope
Plunkr showing fixed isolate scope when replace:false
--- UPDATE 2 ---
I've updated both of the examples to show the issue persisting once the the isolate scope is removed
Plunkr without ng-if and no isolate scope
Plunkr with ng-if and no isolate scope
And also a new version showing the change from templateUrl to template - as suggested by #Manube - that shows the behaviour working as expected
Plunkr with ng-if and no isolate scope using template instead of templateUrl
Using ng-if on a root element of a directive with replace: true creates a broken scope
ISSUE
This is happening with the combination of replace:'true' and ng-if on the root element.
Make sure the contents of your html in the templateUrl has exactly one root element.
If you place ng-if on span
<div >
<span ng-if="item.type == 'test'">{{::item.title}}</span>
</div>
Now why it is happening,it is happening because The ngIf directive removes or recreates a portion of the DOM tree based on an {expression}. If the expression assigned to ngIf evaluates to a false value then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.
which may lead to no root element on the templateUrl while rendering and thus leads to unwanted behaviour.
It is to do with templateUrl, which is asynchronous;
If you replace templateUrl by
template:'<div ng-if="item.type === \'test\'"><span>{{::item.title}}</span></div>'
it will work as expected: see plunker with template instead of templateUrl
the test <div ng-if="item.type === 'test'"> will execute when scope is ready and the templateUrl has been fetched.
As the way the template is fetched is asynchronous, whichever template comes back first executes the test, and displays the item.
Now the question is: why is it always the last template that comes back first?
second one showing in reverse order due to custom directive defined with item-directive name but its not rendering into DOM due to replace=true is used.
for more ref you can refer to this link

Insert directive dynamically and compiling

I have a scenario when a user click a link, I would like to insert a custom element into the DOM for example
//user clicks
$scope.click = function () {
var el = $compile("<my-directive></my-directive>")($scope);
$element.after(el);
};
The my-directive.... directive has an html template.. say for example (template1.html)
<p>My Template for my-directive</p>
{{SomeProperty}}
my-directive is defined like this
module.directive('myDirective', ['$compile', function ($compile) {
return {
restrict: 'E',
replace: true,
templateUrl: '/template1.html',
scope: true
};
}]);
If we assume the scope in myDirective acutally has a value for SomeProperty after running this code I will indeed have the my-directive inserted into the DOM and replaced by the template - template1.html however the {{SomeProperty}} has not been replaced at all! How do I do this??
See Plunkr for more details
You did $compile("<my-directive></my-directive>")($scope.$parent); in your Plunkr. Remove .$parent
Chenge template to:
<p>My Template for my-directive</p>
{{d.SomeProperty}}
As you did d in data.
It works then :)
I'm not sure if you had any other errors, but I found that removing replace:true made it work for me.
I'm not sure exactly what is happening here, but somehow the interaction of you adding the directive to the dom + replacing it immediately causes it not to work.
Another thing I noticed was that by using $element.after(el); you are ending up with an element that is OUTSIDE the controller's scope. It's not in the div that the controller has scope for. Unfortunately, I only saw this in MY plunkr, so don't know if this affected you also.
Plunkr here

In a Directive, passing function arguments through to the html template

I am on day 2 of Angular and am trying to create a directive. The idea is that I have several images of quilts to display and I don't want to repeat the same HTML. Here's an index.html snippet showing a use of the new directive and two 'arguments' I'll need in the partial:
<ng-samplequilt imgPath="img/t_3x3_no_sashing/jpg"
text="This is a 3x3 quilt without sashing.">
</ng-samplequilt>
Here's the partial:
<div>
<img src="{{imgPath}}"/>
<div>
{{text}}
</div>
</div>
Finally, there's the Directive (which may or may not work):
.directive('SampleQuilt', ['imgPath','text',function(imgPath, text) {
return {
restrict: 'E',
templateUrl: 'partials/sample_quilt.html'
};
}])
So I'm clearly a little over my head. I've read a good deal of the docs and some examples, but none seem to be quite what I'm doing. Or perhaps I have not internalized enough for it to stick.
I'm not looking for a full solution here; I don't mind working through it. But I am stuck - I don't know how to get imgPath and text to make their way to the partial where they can be used.
Also, Directives have embedded controllers. How does the partial know to refer to this controller? Why does it even have access to it, given it's buried in the Directive?
Thanks for a boot in the right direction.
EDIT -
Thanks to #Dalorzo I seem to have a solution.
First, his idea about defining the scope in the Directive worked.
Second, I named the directive "SampleQuilt". This did not work - the directive did nothing/could not be found. When I renamed it to sampleQuilt, however, the internal name translation worked. For similar reasons, the HTML had to refer to img-path, not imgPath.
Here are the three files now.
The index.html snippet:
<sample-quilt img-path="img/t_3x3_no_sashing.jpg"
text="This is a 3x3 quilt without sashing.">
</sample-quilt>
The partial:
<div>
<img src="{{img-path}}"/>
<div>
{{text}}
</div>
</div>
The directive:
.directive('sampleQuilt', function() {
return {
restrict: 'E',
scope:{ imgPath: "#", text: "#" },
templateUrl: 'partials/sample_quilt.html'
};
})
;
EDIT 2 -
The above doesn't work - I was getting burned by browser caching.
It seems as if this snippet in index.html is curious...
<sample-quilt img-path="img/t_3x3_no_sashing.jpg"
text="This is a 3x3 quilt without sashing.">
</sample-quilt>
The img-path attribute can apparently be spelled three different ways: img-path, 'imgPath', and img_path. All are converted to imgPath internally. When displaying the value in the partial, imgPath is correct.
Here's the corrected partial:
<div>
<img src="{{imgPath}}"/>
<div>
{{text}}
</div>
</div>
Based on your example above I think this should be what you intent:
var app = angular.module('demo',[]);
app.directive('SampleQuilt', function() {
return {
restrict: 'E',
scope:{ imgPath: "#", text: "#" },
templateUrl: 'partials/sample_quilt.html'
};
});
By adding scope to the directive we create an "isolated scope". With this approach scope can capture attributes in 3 ways:
# Captures the attribute value from the DOM as string value.
= Evaluates the attribute as property of the parent scope.
& Evaluates the attribute as method of the parent scope.
You can read more about it here:
http://onehungrymind.com/angularjs-sticky-notes-pt-2-isolated-scope/
Regarding your html:
Remove ng and don't use it as part of your directives they are reserved by the angular team and it is good to avoid them to avoid conflicts. You can read more about Angular Naming Conventions here
Cases (camel case or pascal case) means dash in angular directives so SampleQuilt needs to be used as sample-quilt in the html.
Sample:
<sample-quilt imgPath="img/t_3x3_no_sashing/jpg"
text="This is a 3x3 quilt without sashing.">
</sample-quilt>
Regarding your last question about the controller on directives. Directives returned object has a controller property that you can use like:
app.directive('SampleQuilt', function() {
return {
restrict: 'E',
controller: 'myDirController', /* <--- Controller Declaration */
scope:{ imgPath: "#", text: "#" },
templateUrl: 'partials/sample_quilt.html'
};
});
app.controller('myDirController', ['$scope', function ($scope) {
// My Directive Controller implementation
}]);

Angular: What is a good way to hide undefined attributes that exist in isolated scopes?

I wanted to rewrite this fiddle as it no longer worked in angular 1.2.1. From this exercise, I learned that a template is apparently always needed now in the isolated scopes.
somewhere in the directive:
template: '<p>myAttr1 = {{myAttr1}} // Passed by my-attr1<br>
myAttr2 = {{myAttr2}} // Passed by my-alias-attr2 <br>
myAttr3 = {{myAttr3}} // From controller
</p>',
I was not able,however, to successfully add this to the template:
<p ng-show="myAttr4">myAttr4= {{myAttr4}} // Hidden and missing from attrs</p>
What is a good way to hide undefined attributes that are defined on the isolated scope but not given a value from the dom?
my humble fiddle
EDIT: I use a directive called my-d1 to encapsulate the bootstrap tags. I use my-d2 to demo how to use the # in isolated scopes.
Working version merged with Sly's suggestions
I ran into the same template issue in Angular 1.2.0, see the first entry in the 1.2.0 breaking changes:
Child elements that are defined either in the application template or in some other directives template do not get the isolate scope. In theory, nobody should rely on this behavior, as it is very rare - in most cases the isolate directive has a template.
I'm not exactly sure what the issue is that you are encountering - it might be some incorrect markup or you are misnaming the scope variables listed in your isolate scope.
Using ng-show will correctly hide the element if the attribute has not been passed in.
i.e. your example here is correct: <p ng-show="myAttr4">myAttr4= {{myAttr4}}</p>
Updated version of your Fiddle: http://jsfiddle.net/Sly_cardinal/6paHM/1/
HTML:
<div ng-app='app'>
<div class="dir" my-directive my-attr1="value one" my-attr3='value three'>
</div>
<div class="dir" my-directive my-attr1="value one" my-attr3='value three' my-attr4='value four'>
</div>
</div>
JavaScript:
var app = angular.module('app', []);
app.directive('myDirective', function () {
return {
// can copy from $attrs into scope
scope: {
one: '#myAttr1',
two: '#myAttr2',
three: '#myAttr3'
},
controller: function ($scope, $element, $attrs) {
// can copy from $attrs to controller
$scope.four = $attrs.myAttr4 || 'Fourth value is missing';
},
template: '<p>myAttr1 = {{one}} // Passed by my-attr1</p> '+
'<p ng-show="two">myAttr2 = {{two}} // Passed by my-alias-attr2 </p>'+
'<p>myAttr3 = {{three}} // From controller</p>'+
'<p ng-show="four">myAttr4= {{four}} // Has a value and is shown</p>'
}
});

Resources