angularjs angular doesn't read the length of an array - arrays

for the passed few days I am struggling with the matter of AngularJS. I am novice at this and that's where my troubles result from.
Anyway here is my problem. I have an app that is made made for asking users some questions, collecting answers and displaying them to the user.
The HTML is:
<div ng-repeat="dialog in dialogWindows">
<div id="{{dialog.idName}}" class="bold abs">
<div class="questionContainer rel">
<button ng-click="compute()>Fake results</button>
<div ng-repeat="input in dialog.inputs">
<input type="radio" id="{{input.radio}}" name="{{dialog.name}}" value="{{input.value}}">
<label for="{{input.radio}}" class="answer abs {{input.a}}">{{input.answer}}</label>
</div>
</div>
</div>
</div><!--/ng-repeat-->
</div><!--/ng-controller-->
And here is the JS managing the ng-repeat above:
function dialogWindows($scope,localStorageService){
$scope.dialogWindows = [
{id:0,
idName:"pigmentation",
number:"1",
name:"Pigmentation",
answer1:"Clear complexion",
answer2:"Semi-swarthy complexion",
answer3:"Swarthy complexion",
answer4:"",
answer5:"",
answer6:"",
href:"#hairColor",
hrefBack:"index.html",
inputs:[{id:0,a:"a1",answer:"Clear compexion", radio:"radio1",value:"1"},
{id:1,a:"a3", answer:"Semi-swarthy complexion", radio: "radio2",value:"1"},
{id:2,a:"a5",answer:"Swarthy complexion",radio:"radio3",value:"1"}
]
Nothing really complicated and so far it works fine. Now you can see that ng-repeat generates three radio buttons. and we have compute function assigned to the button soon you'll see what it does.
Here is the compute() function:
$scope.compute = function() {
if (document.getElementById('radio1').checked) {
$scope.a.push(1);
$scope.b.push(1);
$scope.c.push(1);
$scope.d.push(1);
$scope.e.push(1);
$scope.f.push(1);
$scope.g.push(1);
$scope.h.push(1);
$scope.i.push(1);
$scope.j.push(1);
$scope.k.push(1);
$scope.l.push(1);
$scope.m.push(1);
$scope.n.push(1);
$scope.o.push(1);
$scope.p.push(1);
} else if (document.getElementById('radio2').checked) {
$scope.r.push(1);
$scope.s.push(1);
$scope.t.push(1);
$scope.u.push(1);
$scope.w.push(1);
} else if(document.getElementById("radio3").checked){
$scope.z.push(1);
$scope.x.push(1);
$scope.y.push(1);
$scope.q.push(1);
$scope.ab.push(1);
}
Answered questions are passed to one of 12th arrays responsible for collecting answers.
JS:
$scope.a= [];
$scope.b= [];
$scope.c = [];
$scope.c= [];
$scope.d= [];
$scope.e= [];
$scope.f= [];
$scope.g= [];
$scope.h = [];
$scope.i= [];
$scope.j= [];
$scope.k= [];
$scope.l= [];
$scope.m= [];
$scope.n= [];
$scope.o= [];
$scope.p= [];
$scope.r= [];
$scope.s= [];
$scope.t= [];
$scope.u= [];
$scope.w= [];
$scope.z= [];
$scope.x= [];
$scope.y= [];
$scope.q= [];
$scope.ab= [];
Then I wrote a list of elements each one representic one array, that is...
<div ng-repeat="record in records">
<div class="rel fptsans {{record.className()}}">{{record.item}}</div>
</div>
ng-repeat is generated with this records array as below:
$scope.records = [
{id:0,
className : $scope.a.length > 0 ? 'special' : 'normal',
item: "a",
link: $scope.className == "special" ? "a.html" : ''
},
{id:1,
className: $scope.b.length > 0 ? 'special' : 'normal',
item:"b",
link: $scope.className == "special" ? "b.html" : ''
},
{id:2,
className: $scope.c.length > 0 ? 'special' : 'normal',
item:"c",
link: $scope.className == "special" ? "c.html" : ''
},
//and so on to 12th.
I was sure that every part of the app was consistent but soon I was about to get surprised that Angular doesnt show any results within the ng-repeat="record in records" because it is reffering to an empty objects ($scope.a = []; is in fact empty at initialization), despite that I am able to view the length of an array by simply writing in my html {{a.length}} so apparently the length of an array is increasing.
My question is how may I use $scope.[some array].length inside my angular array. Should I use ng-model with radio buttons ? would it be helpful ? How can I solve this problem which currently made me stuck in one place.Please help I am really out of solutions. Thank You in advance

I think you are populating the $scope.records before the compute is being called. That would result to empty array at the beginning. You need to re-populate the $scope.records by watching your arrays. Look up $watch and see how it works. I personally think you should not put all of those a, b, c, etc. items in the $scope but you should put them in some thing like $scope.Questions.a, $scope.Questions.b, $scope.Questions.c, etc. Then you can create a watch on $scope.Questions or individual items.
[Edit]
I see where is the problem! Before I start to explain the situation I want to recommend you to read more about AngularJS and how to think in AngularJS world (maybe this can help "Thinking in AngularJS" if I have a jQuery background?)
There are a few problems with your code: First you didnt have {{ in your class definition
<div class="record.className}}">
i fixed it:
<div class="{{record.className}}">
the other problem was that you were initializing the ClassName and Link properties of $scope.records in the initialization of the controller and you never updated those values based on the changes in apenic. That means the $scope.Records is being initialized when $scope.alepnic is empty hence both records would have the class "normal". Now if you increase the length of $scope.Records nothing is updating your records to reflect the changes. I moved the initialization code into the compute method so your records are re-evaluated every time your array changes.
the updated code: http://jsfiddle.net/Tb9j5/8/
$scope.compute=function()
{
$scope.alpeic.push(1);
$scope.records = [
{id:0,
link:$scope.alpeic.length > 5 ? "alpeic.html" : "",
className:$scope.alpeic.length > 5 ? "special" : "normal",
item:"This is item A"},
{id:1,
link:$scope.alpeic.length > 5 ? "alpeic.html" : "",
className:$scope.alpeic.length > 5 ? "special" : "normal",
item:"This is item B"}];
}
$scope.compute();
This is not the best Angular method to solve your problem but is the easiest one to communicate to you. The other ways is to setup a $watch on your alpeic array and then re-evaluate the properties in your $scope.Records. Also you could and should use ng-class to set class atrributes without needing a scope:
<div ng-repeat="record in records">
<a href="alpeic.html">
<div ng-class="{special: alpeic.length >= 5, normal: alpeic.length < 5}">{{record.item}}</div>
</a>
</div>

Related

AngularJS ng-repeat update does not apply when object keys stay the same?

I'm trying to make a minimal but fancy AngularJS tutorial example, and I am running into an issue where after updating the entire tree for a model (inside the scope of an ng-change update), a template that is driven by a top-level ng-repeat is not re-rendered at all.
However, if I add the code $scope.data = {} at a strategic place, it starts working; but then the display flashes instead of being nice and smooth. And it's not a great example of how AngularJS automatic data binding works.
What am I missing; and what would be the right fix?
Exact code - select a country from the dropdown -
This jsFiddle does not work: http://jsfiddle.net/f9zxt36g/
This jsFiddle works but flickers: http://jsfiddle.net/y090my10/
var app = angular.module('factbook', []);
app.controller('loadfact', function($scope, $http) {
$scope.country = 'europe/uk';
$scope.safe = function safe(name) { // Makes a safe CSS class name
return name.replace(/[_\W]+/g, '_').toLowerCase();
};
$scope.trunc = function trunc(text) { // Truncates text to 500 chars
return (text.length < 500) ? text : text.substr(0, 500) + "...";
};
$scope.update = function() { // Handles country selection
// $scope.data = {}; // uncomment to force rednering; an angular bug?
$http.get('https://rawgit.com/opendatajson/factbook.json/master/' +
$scope.country + '.json').then(function(response) {
$scope.data = response.data;
});
};
$scope.countries = [
{id: 'europe/uk', name: 'UK'},
{id: 'africa/eg', name: 'Egypt'},
{id: 'east-n-southeast-asia/ch', name: 'China'}
];
$scope.update();
});
The template is driven by ng-repeat:
<div ng-app="factbook" ng-controller="loadfact">
<select ng-model="country" ng-change="update()"
ng-options="item.id as item.name for item in countries">
</select>
<div ng-repeat="(heading, section) in data"
ng-init="depth = 1"
ng-include="'recurse.template'"></div>
<!-- A template for nested sections with heading and body parts -->
<script type="text/ng-template" id="recurse.template">
<div ng-if="section.text"
class="level{{depth}} section fact ng-class:safe(heading);">
<div class="level{{depth}} heading factname">{{heading}}</div>
<div class="level{{depth}} body factvalue">{{trunc(section.text)}}</div>
</div>
<div ng-if="!section.text"
class="level{{depth}} section ng-class:safe(heading);">
<div class="level{{depth}} heading">{{heading}}</div>
<div ng-repeat="(heading, body) in section"
ng-init="depth = depth+1; section = body;"
ng-include="'recurse.template'"
class="level{{depth-1}} body"></div>
</div>
</script>
</div>
What am I missing?
You changed reference of section property by executing section = body; inside of ng-if directives $scope. What happened in details (https://docs.angularjs.org/api/ng/directive/ngIf):
ng-repeat on data created $scope for ng-repeat with properties heading and section;
Template from ng-include $compile'd with $scope from 1st step;
According to documentation ng-if created own $scope using inheritance and duplicated heading and section;
ng-repeat inside of template executed section = body and changed reference to which will point section property inside ngIf.$scope;
As section is inherited property, you directed are displaying section property from another $scope, different from initial $scope of parent of ngIf.
This is easily traced - just add:
...
<script type="text/ng-template" id="recurse.template">
{{section.Background.text}}
...
and you will notice that section.Background.text actually appoints to proper value and changed accordingly while section.text under ngIf.$scope is not changed ever.
Whatever you update $scope.data reference, ng-if does not cares as it's own section still referencing to previous object that was not cleared by garbage collector.
Reccomdendation:
Do not use recursion in templates. Serialize your response and create flat object that will be displayed without need of recursion. As your template desired to display static titles and dynamic texts. That's why you have lagging rendering - you did not used one-way-binding for such static things like section titles. Some performance tips.
P.S. Just do recursion not in template but at business logic place when you manage your data. ECMAScript is very sensitive to references and best practice is to keep templates simple - no assignments, no mutating, no business logic in templates. Also Angular goes wild with $watcher's when you updating every of your section so many times without end.
Thanks to Apperion and anoop for their analysis. I have narrowed down the problem, and the upshot is that there seems to be a buggy interaction between ng-repeat and ng-init which prevents updates from being applied when a repeated variable is copied in ng-init. Here is a minimized example that shows the problem without using any recursion or includes or shadowing. https://jsfiddle.net/7sqk02m6/
<div ng-app="app" ng-controller="c">
<select ng-model="choice" ng-change="update()">
<option value="">Choose X or Y</option>
<option value="X">X</option>
<option value="Y">Y</option>
</select>
<div ng-repeat="(key, val) in data" ng-init="copy = val">
<span>{{key}}:</span> <span>val is {{val}}</span> <span>copy is {{copy}}</span>
</div>
</div>
The controller code just switches the data between "X" and "Y" and empty versions:
var app = angular.module('app', []);
app.controller('c', function($scope) {
$scope.choice = '';
$scope.update = function() {
$scope.data = {
X: { first: 'X1', second: 'X2' },
Y: { first: 'Y1', second: 'Y2' },
"": {}
}[$scope.choice];
};
$scope.update();
});
Notice that {{copy}} and {{val}} should behave the same inside the loop, because copy is just a copy of val. They are just strings like 'X1'. And indeed, the first time you select 'X', it works great - the copies are made, they follow the looping variable and change values through the loop. The val and the copy are the same.
first: val is X1 copy is X1
second: val is X2 copy is X2
But when you update to the 'Y' version of the data, the {{val}} variables update to the Y version but the {{copy}} values do not update: they stay as X versions.
first: val is Y1 copy is X1
second: val is Y2 copy is X2
Similarly, if you clear everything and start with 'Y', then update to 'X', the copies get stuck as the Y versions.
The upshot is: ng-init seems to fail to set up watchers correctly somehow when looped variables are copied in this situation. I could not follow Angular internals well enough to understand where the bug is. But avoiding ng-init solves the problem. A version of the original example that works well with no flicker is here: http://jsfiddle.net/cjtuyw5q/
If you want to control what keys are being tracked by ng-repeat you can use a trackby statement: https://docs.angularjs.org/api/ng/directive/ngRepeat
<div ng-repeat="model in collection track by model.id">
{{model.name}}
</div>
modifying other properties won't fire the refresh, which can be very positive for performance, or painful if you do a search/filter across all the properties of an object.

How Do I Move Objects Inside An ng-Repeat on Button Click?

I have a nifty list of items in an ng-repeat with an up and down button on each. I just want the up button to move the list item up one place and the down button should move it down one place.
The problem is that I get an error saying "Cannot read property 'NaN' of undefined."
It seems "position" is undefined on the second line. What can I do to fix that?
Heres the javascript I'm working with (thanks to Rishul Matta):
$scope.moveUp = function(ind, position) {
$scope.temp = $scope.list[position - 1];
$scope.list[position - 1] = $scope.list[position];
$scope.list[position = temp];
};
Here's my HTML:
<ul>
<li class="steps" ng-repeat="step in selectedWorkflow.Steps track by $index" ng-class="{'words' : step.Id != selectedStep.Id, 'selectedWords' : step.Id == selectedStep.Id}" ng-model="selectedWorkflow.Step" ng-click="selectStep(step, $index); toggleShow('showSubStep'); toggleShow('showEditBtn')">
{{step.Name}}
<input class="orderUpBtn" type="button" ng-click="moveUp($index, step)" style="z-index:50" value="U" />
<input class="orderDownBtn" type="button" style="z-index:50" value="D" />
</li>
</ul>
Thanks!
Thanks for posting this question (+1) and the answer jtrussell (+1). I wanted to share what I believe to be a more re-usable/modular answer for other folks (inspired by odetocode.com post).
For the HTML, jtrussell's code is perfect because he fixed/simplified everything. For a better user experience I just added ng-disabled for the first/last elements.
HTML:
<ul ng-controller="DemoCtrl as demo">
<li ng-repeat="item in demo.list">
{{item}}
<button class="move-up"
ng-click="listItemUp($index)"
ng-disabled="$first">
Move Up
</button>
<button class="move-down"
ng-click="listItemDown($index)"
ng-disabled="$last">
Move Down
</button>
</li>
</ul>
For the JS, Notice the moveItem() function which I believe to be more re-usable. You can use this function for other drag+drop swapping functionality as well.
JS within Controller (tested on Angular 1.3.15):
// Move list items up or down or swap
$scope.moveItem = function (origin, destination) {
var temp = $scope.list[destination];
$scope.list[destination] = $scope.list[origin];
$scope.list[origin] = temp;
};
// Move list item Up
$scope.listItemUp = function (itemIndex) {
$scope.moveItem(itemIndex, itemIndex - 1);
};
// Move list item Down
$scope.listItemDown = function (itemIndex) {
$scope.moveItem(itemIndex, itemIndex + 1);
};
I hope it is helpful to someone out there. Thanks SO community!
A simple list with up/down buttons is pretty straightforward, here's some rough generic code. The ngRepeat directive will honor the order of items in your array so moving things around the view is just a matter of moving them in the array itself.
view:
<ul ng-controller="DemoCtrl as demo">
<li ng-repeat="item in demo.list">
{{item}}
<button ng-click="demo.moveUp($index)">up</button>
<button ng-click="demo.moveDown($index)">down</button>
</li>
</ul>
controller:
app.controller('DemoCtrl', function() {
this.list = list = ['one', 'two', 'three', 'four'];
this.moveUp = function(ix) {
if(ix > -1 && ix < list.length - 1) {
var tmp = list[ix+1];
list[ix+1] = list[ix];
list[ix] = tmp;
}
};
this.moveDown = function(ix) {
// similar...
};
});
There were a few strange items in your code (for example did you mean $scope.list[position] = temp; when you wrote ($scope.list[position = temp];), my example isn't perfect but it should get you going on the right path. Here's the full working demo: http://jsbin.com/vatekodeje, note that in my code I use "up" to mean increasing index rather than toward the top of the page.
Also in your controller you use position as an index (it's not clear that it should be) and make reference to, presumably, an array called $scope.list when in your view you use selectedWorkflow.Steps. Maybe your $scope.list and selectedWorkflow.Steps are meant to be the same thing?

Angular nested ng-repeat filter items matching parent value

I am passing in 2 arrays to my view. I would like my nested loop to only display where it's parent_id value matches the parent.id. Eg.
arr1 = {"0":{"id":326,"parent_id":0,"title":"Mellow Mushroom voucher","full_name":"Patrick","message":"The voucher says $10 Voucher; some wording on the printout says, \"This voucher is valid for $20 Pizza\" but my purchase price or amount paid also says $20. Shouldn't that be $10","type":"Deals"}};
arr2 = {"0":{"id":327,"parent_id":326,"title":"Re: Mellow Mushroom voucher","full_name":"Patrick Williams","message":"Some message here","type":null};
...
<div data-ng-repeat = "parent in arr1">
<span>{{parent.title}}<span>
<div data-ng-repeat="child in arr2 | only-show-where-child.parent_id == parent.id">
<li>{{child.body}}</li>
</div>
</div>
Is this possible/best practice in angular of should I be filtering the object in node before passing it into angular? Thank you!
There are a couple of ways you could do it... You could create a function to return just the children:
$scope.getChildren = function(parent) {
var children = [];
for (var i = 0; i < arr2.length; i++) {
if (arr2[i].parent_id == parent.id) {
children.push(arr2[i]);
}
}
return children;
};
html:
<div ng-repeat="child in getChildren(parent)">
You could define a filter to do the same thing:
myApp.filter('children', function() {
return function(input, parent) {
var children = [];
for (var i = 0; i < input.length; i++) {
if (input[i].parent_id == parent.id) {
children.push(input[i]);
}
}
return children;
};
});
html:
<div ng-repeat="child in arr2|children:parent">
Both of those methods will execute every digest cycle though. If you have a large list of elements you would definitely want to improve performance. I think the best way would be to pre-process those results when you get them, adding a children array to each object in arr1 with only its children (here using array.filter instead of for loop and array.forEach):
arr1.forEach(function(parent) {
parent.children = arr2.filter(function(value) {
return value.parent_id === parent.id;
};
});
Then in the html you are already working with the parent so you can repeat over its children property:
<div ng-repeat="child in parent.children">
Instead of using filters, data-ng-if can achieve the same result.
<div data-ng-repeat="parent in arr1">
<span>{{parent.title}}<span>
<div data-ng-repeat="child in arr2" data-ng-if="child.parent_id == parent.id">
<li>{{child.body}}</li>
</div>
</div>
The solution depends on how often arrays are changed and how big arrays are.
The fist solution is to use filter. But in this case it would be called at least twice (to make sure that result is "stabilized" - selected same elements).
Other solution is to $watch by yourself original array and prepare "view" version of it injecting children there. Personally I would prefer the second as more explicit.
However if you can reuse "find-the0child" filter in other parts of your application you can go with first one - AngularJS will re-run filter only after original array modified.
If needed I can provide here an example of implementation of one of these options - add the comment to answer.

angularjs - resetting li after ngrepeat

I want to flow different data through a user clickable ul but I can't reset the state of the li's which have the isactive style set. Stripping down to bare minimum to demonstrate the input box takes two numbers separated by '-', the first is the number of clickable boxes, the second is the number of unclickable boxes at the beginning.
Note when new input is sent the li's that are currently active remain active. I want to reset the li's to inactive. [ note: trying to do this without jQuery to learn "The Angular Way". I have a pure jQuery version of this ]. angular.copy has not worked (though that could be ignorance)
I'm starting to think this might have to go but I'm keeping the graphic representation exclusively in the .html:
html
<div ng-controller="BoxScreen">
<input type="text" ng-model="inbox" />
<button ng-click="getBox()" /></button>
<div>
<br />
<h2>{{dys}}, {{dst}}</h2>
<div>
<ul class="smallbox">
<li data-ng-repeat="s in skip"> </li>
<li data-ng-repeat="d in ar" ng-class="{'button': !isActive, 'button active': isActive}" ng-init="isActive = false" ng-click="isActive = !isActive; clickMe(d)">{{d}}</li>
</ul>
</div>
</div>
</div>
javascript
angular.module('myApp', [])
.controller('BoxScreen', ['$scope', function($scope) {
$scope.getBox = function() {
indat = $scope.inbox.split('-');
$scope.dys = indat[0];
$scope.dst = indat[1];
$scope.ar = [];
$scope.skip = [];
for(var s=0; s < $scope.dst; s++) {
$scope.skip.push(s);
}
for(var d=1; d <= $scope.dys; d++) {
$scope.ar.push(d);
}
}
$scope.clickMe = function(did) {
//
}
}]);
I believe your problem is related to ng-repeat creating new child scopes for the child elements it attaches to the DOM. When you expand the list with new elements, ng-repeat doesn't actually destroy the old elements (as long as they're unchanged, as is true in your case), but reuse them. See more here.
The way you have designed your structures on the scope seems very messy to me. A better approach is to create all the data beforehand, and not introduce all the logic in the HTML.
Example:
<li data-ng-repeat="d in ar.items" ng-class="{'button': !d.isActive, 'button active': d.isActive}" ng-click="ar.toggle(d)">{{d.text}}</li>
where ar here is an object:
$scope.ar = {
items: [
{
text: '1',
isActive: false
},
more items...
],
toggle: function(d) {
d.isActive = !d.isActive;
}
}
This way you have access to the data in other places as well, and not some hidden away variables set on the child scope.

implement embedding comments like twitter in angularjs

i know in Angular world it is better to bind data than manipulate dom elements. but i can't figure out a way to implement the 'in timeline, click a tweet, load replies, click another tweet load another replies' effects.
here is some code run into my thoughts:
<div class="tweet" ng-repeat="tweet in tweets">
<div class="tweet-content">{{tweet}}</div>
<a class="button" ng-click="loadreplay()">load reply</a>
<div class="reply-container">{{reply}}</div>
</div>
if i write controller like this
app.controller('Test', function($scope){
$scope.tweets = ["foo", "bar"];
$scope.loadreplay = function(){
$scope.reply = "reply";
}
});
then all {{reply}} fields will be filled with 'reply', so in this condition, is manipulate the dom elements the only resolution? or some more "angular" way?
Use a appropriate schema for your data/model. Considering that you would store not only the text but at least something like a ID you would use an object anyway. So think about something like this:
$scope.tweets = [
{ id:1, txt: 'foo' },
{ id:2, txt: 'bar' }
]
Then you could store the individual replies in that object as well:
$scope.loadreply = function(tweet) {
tweet.reply = 'Reply';
}
Note: In this function you could then also use the ID to e.g. fetch the tweets from the server like this:
$scope.loadreply = function(tweet) {
tweet.reply = LoadReplies(tweet.id);
}
You would then use the tweet specific reply attribute for display:
<div ng:repeat="tweet in tweets">
<div>{{tweet.txt}}</div>
<a ng:click="loadreply(tweet)">load reply</a>
<div>{{tweet.reply}}</div>
</div>
See this fiddle for a working demo: http://jsfiddle.net/XnBrp/

Resources