ng-repeat render order reversed by ng-if - angularjs

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

Related

Along with Transclude element, can i pass its scope too to a directive?

Moment i felt i have understood enough about Transclude i came across this statement :
Transclude allows us to pass in an entire template, including its scope, to a directive.
Doing so gives us the opportunity to pass in arbitrary content and arbitrary scope to a directive.
Does this mean, if there is a scope attached to Transclude element and it can be passed on to the directive ? If that's true then am not able to access that scope property inside directive template.
Let me take couple of steps back and explain with code about what am trying to do :
JSFiddle Link
My directive is directive-box and transclude: true is defined in Directive Definition Object(DDO).
Now there is a Child Div, which is the element to be Transcluded
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
and it has controller TransCtrl attached to it.
Now am trying to access $scope.name property which is part of TransCtrl from directive level after defining this in DDO :
scope: {
title: '#directiveTitle',
name: '='
}
Is this possible ?
This is more like a Parent scope trying to access Child scope property, is this permitted in JavaScript Protoypical inheritance ? Or is there something else i need to know ??
If this is not possible what does first statement mean ?
Transclude allows us to pass in an entire template, including its scope, to a directive.
UPDATE 1 :
My primary concern is Controller should remain with Transclude element, still we should be able to pass its (Transclude element) scope to Directive and then Directive should be able to consume that scope i.e., name from TransCtrl controller .
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
Above line of code should remain as is.
I may be completely wrong with my question but please let me if this can be accomplished.
The problem seems to be with the way the controller is defined within the ng-transcluded html.
I have made it clearer by using
the bindToController construct
using a controller at the directive level
Refer this fiddle for a working example.
controllerAs: "TransCtrl",
bindToController: true
And your statement, 'Parent scope trying to access Child scope property' is incorrect right? Since we are trying to use the parent scope property, i.e. name from within the child (ng-transcluded content), which is possible with protypical inheritance, and not the other way around.
Does this answer your question: https://jsfiddle.net/marssfa4/4/?
In it I have created a new controller on the outside (effectively replacing the functionality of your rootScope for inside the directive) and I made the directive's controller be set inside your controller template.
The long and short of it is though that you can see that it is possible to transclude html along with its scope even into a directive with its own scope.
The html:
<div ng-app='myApp' ng-controller="OutsideScope">
<h1>{{externalWorld}}</h1>
<div directive-box directive-title='{{directiveWorld}}' name='name'>
<div>Inside Transclude Scope : {{name}}</div>
</div>
</div>
JS (includes Update 1):
angular.module('myApp', [])
.directive('directiveBox', function() {
return {
restrict: 'EA',
scope: {
title: '#directiveTitle',
name: '='
},
transclude: true,
template: '<div ng-controller="TransCtrl">\
<h2 class="header">{{ title }}</h2>\
<div class="dirContent">Directive Element</div>\
<div>Outside Transclude Scope : {{name}}</div>\
<div class="content" ng-transclude></div>\
</div>'
}
})
.controller('TransCtrl', function($scope) {
$scope.name = 'Transclude World'
})
.controller('OutsideScope', function($scope) {
$scope.name = 'External World'
})
.run(function($rootScope) {
$rootScope.externalWorld = 'External World',
$rootScope.directiveWorld = 'Here comes directive'
});
UPDATE 1: JSFIDDLE
I restored the original scope declarations as the scope: false was a mistake.
If I understand your comment correctly you want to leave the controller on the element to be transcluded but still have the {{name}} within that element ignore its immediate controller and use as controller its parent (i.e. the directive's) scope.
The reason I placed the controller within the template directive is because that is the only way to limit the directive's scope on the directive and not its transcluded elements. If you are explicitly placing a controller on an element, then regardless of whether it is contained within a directive with another scope, its closest scope will override whatever scope has been declared on the directive. In other words, regardless of what the directive's scope is, the {{name}} in
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
will always be whatever $scope.name is in TransCtrl.

Order AngularJS directives are created not as I understand from the documentation

I have an AngularJS app where I have to traverse some of the elements with the cursor keys. To do this I created a directive called selectable that adds some info to a list in a service when the directive is created.
It is important that this happens in the order that the selectable directives appear in the view.
From my research:
Directive compilation step by step and other
I thought that this would work because pre-link and controllers would be created in order. But this does not always happen.
Here is my HTML:
<selectable ng-repeat="suggestion in pCtrl.suggestions" value="suggestion">
{{suggestion}}
</selectable>
<div ng-repeat="cat in pCtrl.categories">
<selectable ng-repeat="item in cat" value="item">
{{item}}
</selectable>
</div>
<selectable ng-if="true" value="pCtrl.bottom">
<div>
Bot content
</div>
</selectable>
And here the directive:
app.directive('selectable', function() {
return {
restrict: 'E',
replace: true,
scope: {},
controller: 'SelectableCtrl',
controllerAs: 'selCtrl',
bindToController: {
value: '='
}
};
})
.controller('SelectableCtrl', [
function() {
var self = this;
console.log(this.value);
}
]);
What I see in the console log is that the bottom selectable with the ng-if is created just after the first ng-repeat and then the rest of the ng-repeats are created.
I made a plunker to demonstrate what happens. Please check the console log of the plunker.
Plunker: Directive creation order test
In angular JS Compilation order of nested directives based on priority
the deeper the element nested, the later it is compiled.
In your code
selectable for the Categories are nested in the div
div ng-repeat="cat in pCtrl.categories"
first the selectable directives which are present in the outer div tag gets compiled
and then remaining selectable directive which is present in inner div tag gets compiled.
Hence the console output showing the order as per its compilation.

directive's scope inside ng-repeat 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'
}
});

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

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