ng-repeat inside ng-if rendering despite falsey ng-if expression - angularjs

I have an ng-repeat that iterates over an array of objects. Each object has a type property and a contents array. Inside the ng-repeat, I would like to display the contents array different depending on the type of the object, so I am conditionally displaying content with an ng-if.
The problem is it appears the ng-repeat is executing and rendering regardless of if the ng-if is truthy or falsey, and that when ng-if is falsey that it's not removing the ng-repeat DOM elements.
I'm not sure if there's something wrong with the implementation, or if these angular directives aren't meant to be used quite like this - or if there's a better way to solve this problem in angular.
Below are the snippets, and here is a working plunker: http://plnkr.co/edit/FZQ5CM8UFOepNIHHuTD4?p=preview
The HTML:
<div ng-controller="FunkyCtrl as vm">
<div ng-repeat="thing in vm.twoThings">
<p ng-if="thing.type === 'apple'">
<span>Type: {{thing.type}}</span>
<ul>
<li ng-repeat="content in thing.contents">Food: {{content.stuff}}</li>
</ul>
</p>
<p ng-if="thing.type === 'dog'">
<span>Type: {{thing.type}}</span>
<ul>
<li ng-repeat="content in thing.contents">Guts: {{content.stuff}}</li>
</ul>
</p>
</div>
</div>
The script:
(function(angular) {
angular.module('funkyStuff', []);
angular.module('funkyStuff').controller('FunkyCtrl', FunkyCtrl);
function FunkyCtrl() {
var vm = this;
vm.twoThings = [
{
type: 'apple',
contents: [
{
stuff: 'apple slices'
},
{
stuff: 'juice'
}
],
},
{
type: 'dog',
contents: [
{
stuff: 'bones'
},
{
stuff: 'meat'
}
]
}
];
}
})(angular);
Expected Output:
Type: apple
Food: apple slices
Food: juice
Type: dog
Guts: bones
Guts: meat
Actual Output:
Type: apple
Food: apple slices
Food: juice
Guts: apple slices
Guts: juice
Food: bones
Food: meat
Type: dog
Guts: bones
Guts: meat

It's because your HTML is invalid. The ul element is not allowed inside p.
If you inspect the rendered HTML you will notice that the ul has been rendered outside the p (might depend on the browser implementation of course).
Replace p with for example div and you will get the result you expect.
Demo: http://plnkr.co/edit/pBVZ4jIJ5qzRsa8xZbud?p=preview
If you want to know which elements are allowed inside p you can find a good answer here.

Related

How to sort on input text in Angular?

I am new to Angular. I have an input text box where I am going to enter sorting criteria like "rating" and "date". I have a controller defined with data.
comments: [{
rating:5,
date:"2012-10-16T17:57:28.556094Z"
},
{
rating:4,
date:"2014-09-05T17:57:28.556094Z"
}]
How can I define filters which will do the sorting based on input?
Working Plnkr
In this example, when you type rating or date in input text field, it will sort the data based on this value. I have called the method on key press. If you want to sort after clicking on sort button, add a button and call he sorting method on ng-click.
var myApp = angular.module('myApp',[]);
myApp.controller('MyController',function($scope,$filter){
$scope.test = 'This is a test!';
$scope.comments = [
{
rating:5,
date:"2012-10-16T17:57:28.556094Z"
},
{
rating:4,
date:"2014-09-05T17:57:28.556094Z"
},
{
rating:7,
date:"2010-09-05T17:57:28.556094Z"
}];
$scope.comments2 = $scope.comments;
$scope.$watch('sortItem', function(val)
{
if(val==='rating' || val==='date'){
$scope.comments = $filter('orderBy')($scope.comments2,val);
}
else{
return;
}
});
});
Hope that solve your problem.
You can use a collections library like underscorejs, and it will be like,
Bind your input box value to a variable as,
<select ng-model=inputVal></select>
Inside your controller function,
$scope.arr = _.sortBy($scope.originalArray, function(o) {
return o[$scope.inputVal];
})
Is this process not working?
<div ng-app ng-controller="myCtrl">
<input type="text" ng-model="sortOption" />
<ul>
<li ng-repeat="comment in comments | orderBy: sortOption">
rate: {{comment.rating}}
</li>
</ul>
AngularJS
$scope.sortOption = "rating";
$scope.comments = [
{
rating:4,
date:"2015-10-16T17:57:28.556094Z"
},
{
rating:5,
date:"2014-09-05T17:57:28.556094Z"
}
]
In your controller add this:
this.sortOption = ''
Call the sortOption above on your input text as follow:
ng-model="sortOption"
Finally modify your ng-repeat as follow:
ng-repeat="comment in YourControllerName.comments|orderBy: sortOption"
In a nutshell :if you type :date then it sorts by date. If you type author then it sorts by author, if you type rating then it sorts by rating.
All you need to do is to apply ng-model to your input to create a binding no need for ng-click. With two way data binding once the input field is touched the filter will be applied so:
<input type="text" ng-model="orderKey" name="orderField">
then using the repeat directive
ng-repeat = "comment in comments | orderBy:orderKey"
Pay attention to spacing between orderBy and the (:) NO SPACE

Something like "with variable" in AngularJS?

Sample controller data:
$scope.items = [{ id: 1, name: 'First'}, { id: 2, name: 'Second'}];
Is there something in angular to make the following code work like "with variable"?
<ul>
<li data-ng-repeat="items">{{id}} {{name}}</li>
</ul>
Instead of:
<ul>
<li data-ng-repeat="i in items">{{i.id}} {{i.name}}</li>
</ul>
Please feel free to make a more understandable title/question.
Referring to Angular ngRepeat document, currently only followings expressions are supported.
variable in expression
(key, value) in expression
variable in expression track by tracking_expression
variable in expression as alias_expression
This means, you can't simply use ng-repeat="items" to iterate the collection.
BTW, ng-repeat will create a separate scope for each element and bind variable or (key, value) to the newly created scope. So "with variable" you refer to is not Angular built-in. You need to create a customized directive for this functionality.
My preferred answer would be "don't do this" but failing that, and because it's interesting, here's a proof of concept, assisted by this question and mostly adapted from this blog post:
app.directive('myRepeat', function(){
return {
transclude : 'element',
compile : function(element, attrs, linker){
return function($scope, $element, $attr){
var collectionExpr = attrs.myRepeat;
var parent = $element.parent();
var elements = [];
// $watchCollection is called everytime the collection is modified
$scope.$watchCollection(collectionExpr, function(collection) {
var i, block, childScope;
// check if elements have already been rendered
if(elements.length > 0){
// if so remove them from DOM, and destroy their scope
for (i = 0; i < elements.length; i++) {
elements[i].el.remove();
elements[i].scope.$destroy();
};
elements = [];
}
for (i = 0; i < collection.length; i++) {
// create a new scope for every element in the collection.
childScope = $scope.$new();
// ***
// This is the bit that makes it behave like a `with`
// statement -- we assign the item's attributes to the
// child scope one by one, rather than simply adding
// the item itself.
angular.forEach(collection[i], function(v, k) {
childScope[k] = v;
});
// ***
linker(childScope, function(clone){
// clone the transcluded element, passing in the new scope.
parent.append(clone); // add to DOM
block = {};
block.el = clone;
block.scope = childScope;
elements.push(block);
});
};
});
}
}
}
});
And then this will do what you want:
app.controller("myController", function($scope, $http) {
$scope.items = [
{a: 123, b: 234},
{a: 321, b: 432}
];
});
With the HTML structure you want:
<div ng-controller="myController">
<ul>
<li my-repeat="items">
{{ a }} {{ b }}
</li>
</ul>
</div>
Notice that given the attributes are copied into the child scopes, rather than referenced, if changes are made to the view, they won't affect the model (ie. the parent items list), severely limiting the usefulness of this directive. You could hack around this with an extra scope.$watch but it'd almost certainly be less fuss to use ng-repeat as it's normally used.
I can't see why other users are telling you that what you want has to be done via a new directive. This is a working snippet.
angular.module("Snippet",[]).controller("List",["$scope",function($scope){
$scope.items = [{ id: 1, name: 'First'}, { id: 2, name: 'Second'}];
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="Snippet" ng-controller="List as list">
<ul>
<!-- Iterating the array -->
<li ng-repeat="item in items">
<!-- Iterating each object of the array -->
<span ng-repeat="(key,value) in item">{{value}} </span>
</li>
</ul>
</body>
Simply, you need to iterate the elements of the array via ng-repeat, then you can do what you want with the object item retrieved. If you want to show its values, for example, as it seems for your question, then a new ng-repeat gets the job done.

Conditionally apply hasDropdown directive on ng-repeated element

I'm working on a project where I use both angularJS and foundation, so I'm making use of the Angular Foundation project to get all the javascript parts of foundation working. I just upgraded from 0.2.2 to 0.3.1, causing a problem in the top bar directive.
Before, I could use a class has-dropdown to indicate a "top-bar" menu item that has a dropdown in it. Since the menu items are taken from a list and only some have an actual dropdown, I would use the following code:
<li ng-repeat="item in ctrl.items" class="{{item.subItems.length > 0 ? 'has-dropdown' : ''}}">
However, the latest version requires an attribute of has-dropdown instead of the class. I tried several solutions to include this attribute conditionally, but none seem to work:
<li ng-repeat="item in ctrl.items" has-dropdown="{{item.subItems.length > 0}}">
This gives me a true or false value, but in both cases the directive is actually active. Same goes for using ng-attr-has-dropdown.
this answer uses a method of conditionally applying one or the other element, one with and one without the directive attribute. That doesn't work if the same element is the one holding the ng-repeat so i can't think of any way to make that work for my code example.
this answer I do not understand. Is this applicable to me? If so, roughly how would this work? Due to the setup of the project I've written a couple of controllers and services so far but I have hardly any experience with custom directives so far.
So in short, is this possible, and how?
As per this answer, from Angular>=1.3 you can use ngAttr to achieve this (docs):
If any expression in the interpolated string results in undefined, the
attribute is removed and not added to the element.
So, for example:
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
angular.module('app', []).controller('testCtrl', ['$scope',
function ($scope) {
$scope.ctrl = {
items: [{
subItems: [1,2,3,4], name: 'Item 1'
},{
subItems: [], name: 'Item 2'
},{
subItems: [1,2,3,4], name: 'Item 3'
}]
};
}
]);
<div ng-app="app">
<ul ng-controller="testCtrl">
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
{{item.name}}
</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
Ok, I made a directive. All <li> will need an initial attr of:
is-drop-down="{{item.subItems.length > 0}}"
Then the directive checks that value and for somereason its returning true as a string. Perhaps some onc can shed some light on that
app.directive('isDropDown', function () {
return {
link: function (scope, el, attrs) {
if (attrs.isDropDown == 'true')
{
return el.attr('has-dropdown', true); //true or whatever this value needs to be
}
}
};
});
http://jsfiddle.net/1qyxrcd3/
If you inspect test2 you will see it has a has-dropdown attribute. There is probably a cleaner solution, but this is all I know. I'm still new to angular.
edit I noticed a couple extra commas in my example json data..take note, still works, but they shouldn't be there.

AngularJS - FAQ inside a modal (bug?)

Currently i'm developing a webapp with AngularJS for a giant company, and i'm trying to have a simple FAQ inside a modal.
In my localhost the FAQ it's working just fine (very similar to the original FAQ in angular documentation), but when i write exactly the same code inside a modal i'm getting a console error:
TypeError: Object [object Object] has no method 'addGroup'
Important to state that inside the modal my $scope.oneAtATime = true; it's being ignored, so basically even if i force it to be true
<accordion close-others="true">
It's always false.
This addGroup method is on the AngularJS library code.
Any ideas?
The HTML:
<div class="modal__container__body">
<div id="faq_accordion" ng-controller="AccordionController">
<accordion close-others="true">
<accordion-group heading="{{faq.title}}" ng-repeat="faq in faqs">
{{faq.content}}
</accordion-group>
</accordion>
</div>
</div>
The controller
lobby.controller("AccordionController", ["$scope", function ($scope) {
$scope.oneAtATime = true;
$scope.faqs = [
{
title: "Q1?",
content: "A1"
},
{
title: "Q2?",
content: "A2"
},
{
title: "Q3?",
content: "A3"
},
{
title: "Q4?",
content: "A4"
}
];
}]);
Please notice that in the above code i'm forcing close-others to be true, directly in the html tab.
Help?
We had the same problem recently, change
<div id="faq_accordion" ng-controller="AccordionController">
to
<div id="faq_accordion" ng-controller="MyAccordionController">
That should fix it. You basically overwrote the plugin controller with your own. Don't forget to change the controller definition also, it's the part that's breaking it.

angularjs - ngRepeat with ngInit - ngRepeat doesn't refresh rendered value

I have array which is displayed using ngRepeater but with this directive I'm using ngInit directive which execute function which should return object to be displayed. Everything works perfectly but when I added "New" button where I add new value to array then function "SetPreview" is executed only once I think function should be executed depending from the amount of array value. How can I do that?
UI:
<body ng-app>
<ul ng-controller="PhoneListCtrl">
<button ng-click="add()">Add</button>
<li ng-repeat="phone in phones" ng-init="displayedQuestion=SetPreview(phone);">
{{displayedQuestion.name}}
<p>{{displayedQuestion.snippet}}</p>
</li>
</ul>
</body>
Controller:
function PhoneListCtrl($scope) {
$scope.phones = [
{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S."},
{"name": "Motorola XOOM™ with Wi-Fi",
"snippet": "The Next, Next Generation tablet."},
{"name": "MOTOROLA XOOM™",
"snippet": "The Next, Next Generation tablet."}
];
$scope.add = function(){
this.phones.push({name:'1', snippet:'n'});
};
$scope.SetPreview = function(phone)
{
//here logic which can take object from diffrent location
console.log(phone);
return phone;
};
}
Sample is here - jsfiddle
Edit:
Here is more complicated sample: -> Now collection of Phones is empty, when you click Add button new item is added and is set as editable(you can change value in text field) and when ngRender is executed SetPreview function returns editable object(it’s work like preview). Now try click Add button again and as you can see the editable value of first item is still presented to user, but I want to refresh entire ng-repeater.
You are running into an Angular performance feature.
Essentially Angular can see that the element in the array ('A' for example) is the same object reference, so it doesn't call ng-init again. This is efficient. Even if you concatenated an old list into a new list, Angular would see that it it the same reference.
If instead you create a new object with the same values as the old object, it has a different reference and Angular re-inits it:
Bad example that does what you are looking for: http://jsfiddle.net/fqnKt/37/
$scope.add = function(item) {
var newItems = [];
angular.forEach($scope.items, function(obj){
this.push({val:obj.val});
},newItems)
newItems.push({val:item})
$scope.items = newItems;
};
I don't recommend the approach taken in the fiddle, but rather you should find a different method than ng-init to trigger your code.
I've found out that you can just replace ng-init with angular expression {{}} in a hidden block:
<body ng-app>
<ul ng-controller="PhoneListCtrl">
<button ng-click="add()">Add</button>
<li ng-repeat="phone in phones">
<span style="display: none">{{displayedQuestion=SetPreview(phone)}}</span>
{{displayedQuestion.name}}
<p>{{displayedQuestion.snippet}}</p>
</li>
</ul>
</body>
If you have an ngInit in an element, and an ngRepeat in a descendant, the ngInit will only run once.
To fix that, add an ngRepeat to the element with the ngInit, with an expression that repeats once and depends upon the same array (and other dependencies).
For example:
ng-repeat="_ in [ products ]" ng-init="total = 0"

Resources