Angular: Getting list with ng-repeat with dividers / separators - angularjs

Still pretty new with Angular, just finding my way around.
I'm using ng-repeat to output an alphabetised list of names. I'd like to add dividers within this list that act as labels.
Example:
--------
A
--------
Author 1
Author 2
--------
B
--------
Author 3
Author 4
etc
My thinking is to use nested ng-repeats to loop through the alphabet, getting an object with the authors for that specific letter with a second ng-repeat. Here's what I have so far:
<div data-ng-repeat="letter in alphabet">
<div class="item item-divider">
{{letter}}
</div>
<ul>
<li data-ng-repeat="speaker in GetSpeakers(letter)" type="item-text-wrap" href="#/speaker/{{speaker.ID}}">
{{speaker.title}}
</li>
</ul>
</div>
Controller code:
.controller('SpeakersCtrl', function($scope, $routeParams, StorageHandler) {
$scope.GetSpeakers = function(letter) {
// Get list of authors for that letter
console.log('test '+letter);
};
$scope.alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
})
Fiddle: http://jsfiddle.net/t6Xq8/
I have a couple of questions.
In general, is using a nested ng-repeat a good approach for this
problem, or does Angular have built-in specifically for this purpose? Some sources also say using a function in ng-repeat is a bad idea. But it does work so I'm confused as to why I shouldn't use this.
When looking at the console, GetSpeakers gets called twice in this example and I can't figure out why?
How should I return an object to the scope within the GetSpeakers function, while preventing overloading the $scope?

I think you can do this in a much simpler way without:
having to do a nested ng-repeat
writing out the whole alphabet
needing to handle letters which have no author
manipulating your original array of objects or copying it
writing much fewer lines of code
by using $index.
so that would look something like this:
<div ng-repeat="speaker in speakers | orderBy : 'name'">
<div class="item item-divider" ng-if="firstLetter(speaker.name) != firstLetter(speakers[$index-1].name)">
{{firstLetter(speaker.name)}}
</div>
{{speaker.name}}
</div>
This would need a simple function to get the first letter such as:
function firstLetter(name) {
return name && name.charAt(0);
}
So what this does is it compares the first letter of whatever you passed to the previous object's first letter and if they are different it adds the divider with that letter. Pretty clean and simple :)
Check out this working JsFiddle
You can obviously improve on that code to handle upper/lowercase (i.e. always capitalize before comparing) as well as extract the comparison into a function for cleaner code.

Likely better to map the data into a single object where the object keys are the letter. I'll assume you have objects like:
{id:123, firstName:'Frank',lastName :'Enstein'}
and want the letter to represent last names
var tmp={};
for(i=0;i<authourArray.length;i++){
var letter=authourArray[i].lastName.charAt(0);
if( tmp[ letter] ==undefined){
tmp[ letter]=[]
}
tmp[ letter].push( authourArray[i] );
}
/* likely want to loop over all the arrays now and sort unless already sorted from server*/
$scope.repeaterObject=tmp;
Now in markup will ng-repeat all the letters in the object keys, and within that loop, do an ng-repeat of the authors array for that letter
<div data-ng-repeat="(letter, authors) in repeaterObject">
<div class="item item-divider">
{{letter}}
</div>
<ul>
<li ng-repeat="author in authors">{{author.firstName}} {{author.lastName}}</li>
</ul>
</div>
Resulting object will look like:
{
A:[.....],
......
E:[ {id:123, firstName:'Frank',lastName :'Enstein'}, /* other E author objects*/ ],
......
}

I believe that using nested ng repeats is perfectly fine. Im not familiar with a better way that angularjs provides to iterate over multi dimensional arrays/data structures.
The reason you should avoid using functions in ng repeats is that angularjs will call that function each time it will create the element in the repeat directive. As charlie suggested above it would be better to order the authors once and use the resulting array each time, rather than ordering the authors each time you display them. This has the added benefit of being able to reuse the array

Related

AngularJS - track inner $index across multiple ng-repeat loops

I'm attempting to list out a nested data structure while maintaining the inner $index as a running index.
To illustrate, this is what I want to achieve:
Category Name 1
1. Foo
2. Bar
3. Fez
Category Name 2
4. Qux
5. Baz
And this is the result I get now:
Category Name 1
1. Foo
2. Bar
3. Fez
Category Name 2
1. Qux
2. Baz
As you can see, I'd like for the index to carry over between parent loops. A simple problem to solve in regular programming, but it's proving a bit difficult to do in the angularjs loops themselves.
Here is a boiled-down version of what is currently running, the custom number property on the li element being the tricky bit.
<div ng-repeat="(category, items) in groups">
<h2 class="category-name">{# category #}</h2>
<ul class="items-list">
<li id="{# item.id #}" my-widget ng-repeat="item in items" number="$index">
</li>
</ul>
</div>
The data is structured as such (the boiled-off code consumes the objects):
{
'Category Name 1': [{id:1},{id:2}],
'Category Name 2': [{id:3},{id:4}]
}
Note that there is no guarantee that the ids will/should be sequential.
I've attempted to use a scope variable and attempted to increment it in the loop, but it always shows as the last number:
// Controller.js
$scope.count = 0
// view.html
<li ... number="count + 1"></li> <!-- or count++ or any obvious variation -->
Or even something as ugly as:
... </li>
<p hidden>{# increment() #}</p>
// Controller.js
$scope.increment = function(){$scope.count++;};
In addition, these just make AngularJS explode with error messages regarding $apply.
I've also attempted to use ng-init in the parent loop to store the loop index, but I must be misunderstanding how to use it properly. Apologies for not showing any more work, but I'm having trouble getting anywhere other than where I am..
What can I do to carry over the loop?
Any help would be greatly appreciated!
Add global index property to item and calculate it.
Or if you dont want to change item object create it as map:
$scope.myIndexMap = {};
globalIndex = 0;
forEach..., forEach...
globalIndex++;
$scope.myIndexMap[item.id] = globalIndex;
P.S. lots of people tries to do weird and complicated things in html -- better do not. View should be as simple as possible.

Angularjs: Ng-repeat and unique filter in array

I have a set of data like this
$scope.students = [
{ name:"John", courses:["Math", "Physics", "Chemistry"] },
{ name:"Paul", courses:["Economics", "Math", "Sociology"] }
]
I would like a way to filter using angular-filter so that I can get a list of all the subjects without repetition.
I've been trying to use unique filter, but I cannot get it to work since I try to iterate like
<ul ng-repeat="student in students">
<li ng-repeat="x in student.courses | unique:courses"></li>
{{x}}
</ul>
My desired output of the first ng-repeat would be an array like this:
["Math", "Physics", "Chemistry", "Economics", "Sociology"]
so I could iterate through it in the second one.
I have achieved this throught making a new scope with just the desired array, but then I cannot bind it properly, so I would like to achieve it through filtering. Thanks in advance.
I would really recommend you using the libraries Lodash or Underscore for this kind of problems. Learning to master these has helped me a lot!
Of course, you can create your own angular filter using one of them. The method you would like to use is union:
_.union(_.flatten(_($scope.students).pluck("courses")))
I use pluck to get out the courses arrays from the studens object, then I flatten the result (to get rid of the array that it comes nested in), and then i use union to get each subject only once.
If the requirement actually is to:
...get a list of all the subjects without repetition.
Then I would make a separate array for subjects:
$scope.courses = [];
// There are many ways to do skin this cat... (prograde does have a point!)
$scope.students.forEach(function(student) {
student.courses.forEach(function(course) {
if ( $scope.courses.indexOf(course) === -1 ) {
$scope.courses.push(course);
}
})
});
HTML
<ul>
<li ng-repeat="course in courses">
{{ course }}
</li>
</ul>
And if the students change I would recreate the courses-array.
But it sounds as if you are trying to do something else.
But it doesn't give the desired output.
It would help if you tell us what that desired output is!
http://plnkr.co/edit/bZNjIEFznvuyTU3zxAp1?p=preview

How can I accomplish this task in AngularJS?

I am attempting to accomplish a task where I am using AngularJS so I am not sure how to implement potentially needed Javascript or if there is a way with using AngularJS.
I have this input:
<input type="number" ng-model="myCount" ng-disabled="button" min="1" value="1" id="myValue">
This code takes the value input by the user and places it in a variable:
$scope.myCount = document.getElementById("myValue").value;
Now, what I want is based on the number input, I want an array/object created for the number from the variable. For example:
for(i=0;i<myCount.length;i++){
$scope.trackObject=[
{name:i + "Objects"}
]
}
Here is what I am trying to accomplish with this:
<ul class="nav nav-tabs" id="pizza_tabs" role="tablist" ng-repeat="x in countPizzas">
<li class="active">{{x.name}}</li>
</ul>
Sorry, I don't think I very well explained what I am trying to accomplish!
I want there to be a tab that contains the text "# Object", for each object created which the number of objects created is based on the user input. Does that make sense? So, if the user inputs 3, then 3 objects will be created, which I will be able to assign a name and whatever other properties to. 3 tabs are thusly created for the user to toggle through that will contain these properties. I hope that helps!
"what I want is based on the number input, I want an array/object created for the number from the variable"
for(i=0;i<myCount.length;i++){
$scope.trackObject=[{name:i + "Objects"}]
}
//Try this
var name;
for(i=0;i<myCount;i++){ //because myCount is a number
name = i+"Objects";
$scope.trackObject.push({name:name}); //add to the array
}
Now for your last snippet, I didn't understand what you want to accomplish there.
I am confused by your question.
You have already set the ngModel to myCount so whatever you have typed into the input box would have been set to the ngModel - 'myCount'
There is no need to use jquery to get the value as such
"$scope.myCount = document.getElementById("myValue").value;"
As for the second part, i believe the user "I_Wrote_That" has already replied you on how to create an array of objects based on the input.
At first, angular supports two way data binding. So you need not get the value of the element manually using document.getElementById etc.
$scope.myCount = document.getElementById("myValue").value;
Instead the value will be available in myCount by default.
Now to generate the objects based on count:
<ul class="nav nav-tabs" id="pizza_tabs" role="tablist">
<li class="active">{{x.name}}</li>
</ul>
Now in your controller:
$scope.generateCounts = function(myCount) {
$scope.trackObject = [];
for(var i=0; i < myCount; i++) {
$scope.trackObject.push({name : i+"objects"});
}
return $scope.trackObject;
}
But this will not work still, it will give you duplicate key error.
So in your HTML, use track by $index
<li class="active">{{x.name}}</li>

How to set a boolean flag to collapse/expand a row with ng-repeat

I have this plunker code.
What I'm trying to do, is to display the gray box one time per row.
To achieve this, I thought to modify the partition filter in order to return a JSON to add it a new property by row to know if the gray box is expanded or not.
But, I could Not successfully return a JSON.
Do you know how to modify the filter to return a JSON or a better way to show the gray box by row?
Related questions:
Push down a series of divs when another div is shown
Update 1
The issue could be easily resolved by using the correct scope for the ng-repeat for the row without modifying the filter, thanks to #m59.
http://plnkr.co/edit/eEMfI1lv6z1MlG7sND6g?p=preview
Update 2
Live Demo
If I try to modify the item, it seems the ng-repeat would be called again losing the props values.
<div ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}">
<div ng-repeat="item in friendRow"
ng-click="collapse(item)"
ng-class="{myArrow: showArrow}">
{{item.name}} {{item.age}} years old.
<div>{{item.name}}</div>
</div>
<div collapse="!props.isExpanded">
some content
<br/>
<input type="text" ng-model="currentItem.name">
</div>
</div>
js
$scope.collapse = function(item){
this.props.isExpanded = !this.props.isExpanded;
this.showArrow = !this.showArrow;
$scope.currentItem = item;
};
This causes the gray box to collapse each time the item is modified. Any clue?
I've updated my code/answer regarding partitioning data. It's important to fully understand all of that before deciding on an approach to your project.
The problem you have in your plnkr demo is that you're modifying the parent $scope and not the scope of the ng-repeat for that row.
Just set a flag on the row and toggle it when clicked:
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="isExpanded = false"
ng-click="isExpanded = !isExpanded"
>
<div ng-repeat="item in friendRow">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!isExpanded">
some content
</div>
</div>
To access the correct scope within a function in the controller, you can use the this keyword instead of $scope. this will refer to the scope the function is called from, whereas $scope refers to the scope attached to the element with ng-controller (a parent of the ng-repeat scopes you want to target).
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-click="collapse()"
>
JS:
$scope.collapse = function() {
this.isExpanded = !this.isExpanded;
};
If you want to keep the ng-click directive on the item element instead of putting it on the row element as I have done, then you're dealing with another child scope because of that inner ng-repeat. Therefore, you will need to follow the "dot" rule so that the child scope can update the parent scope where the collapse directive is. This means you need to nest isExpanded in an object. In this example, I use ng-init="props = {}", and then use props.isExpanded. The dot rule works because the children share the same object reference to props, so the properties are shared rather than just copied, just like in normal JavaScript object references.
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}"
>
<div ng-repeat="item in friendRow" ng-click="collapse()">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!props.isExpanded">
some content
</div>
</div>
JS:
$scope.collapse = function(){
this.props.isExpanded = !this.props.isExpanded;
};
Update
We keep going through more and more issues with your project. You really just need to experiment/research and understand everything that's going on on a deeper level, or it will just be one question after another. I'll give it one last effort to get you on the right track, but you need to try in the basic concepts and go from there.
You could get past the issue of props reinitializing by putting $scope.expandedStates and then passing the $index of the current ng-repeat to your function (or just using it in the view) and setting a property of expandedStates like $scope.expandedStates[$index] = !$scope.expandedStates[$index]. With the nested ng-repeat as it is, you'll need to do $parent.$index so that you're associating the state with the row rather than the item.
However, you'll then have another problem with the filter: Using my old partition code, the inputs inside the partitions are going to lose focus every time you type a character. Using the new code, the view updates, but the underlying model will not. You could use the partition filter from this answer to solve this, but from my understanding of that code, it could have some unexpected behavior down the road and it also requires passing in this as an argument to the filter. I don't recommend you do this.
Filters are meant to be idempotent, so stabilizing them via some kind of memoization is technically a hack. Some argue you should never do this at all, but I think it's fine. However, you definitely should ONLY do this when it is for display purposes and not for user input! Because you are accepting user input within the partitioned view, I suggest partitioning the data in the controller, then joining it back together either with a watch (continuous) or when you need to submit it.
$scope.partitionedFriends = partitionFilter($scope.friends, 2);
$scope.$watch('partitionedFriends', function(val) {
$scope.friends = [].concat.apply([], val);
}, true); // deep watch

Ambiguity in the use of ngRepeat

I have following problem with using of AngularJS ngRepeat.
The issue can be viewed in this jsFiddle.
http://jsfiddle.net/zono/9rmEs/2/
The user can choose character and after this get all combination
of chosen characters in alphabet. Eg:
A - A-B, A-C, A-D and etc.
B - B-A, B-C, B-D and etc.
Everithing works properly but when user change value of selected
character the combination does not get updated. I solved this problem
with adding following code.
<span style="display: none;">
{{item.combionations = getCombinations(item)}}
</span>
And "hack" it. But there must be normal solution.
I would be very grateful for any ideas and recommendations.
Best regards.
Update
In case you plan to do more complex calculations based on the selection this simplified approach would not work. In general it is also better to encapsulate state in some data structure. In your case you could design a structure like this:
{ letter: "A", combinations: ["A-B", "A-C", ... ] }
To update the combinations array you can use ng-change="updateItem(item)" and some update function. Whenever you change the selection the array combination gets updated:
$scope.updateItem = function(item) {
item.combinations = getCombinations(item.letter);
}
I put this in a new fiddle.
You can easily solve this issue by using the model you bound to ng-select in the ng-repeat.
In the select you used item.model. Angular will update its value in the scope whenever you change the selection.
<select data-ng-model="item.model" ng-init="item.model=allLetters[0]" ng-options="value for value in allLetters">
</select>
When you use the same scope variable in ng-repeat you should get the desired behavior.
<div ng-repeat="letter in allLetters">
{{item.model}}-{{letter}}
</div>
Take a look an the updated fiddle.
The problem is that you compute combionations once at the begenning (ng-init="item.combionations=getCombinations(item)"). After that it never gets updated when you change item.model.
You could solve this problem (and also make sure created[...].combionations is kept up-to-date) like this:
<div data-ng-repeat="item in created">
<div ng-repeat="combination in item.combionations = getCombinations(item)">
{{combination}}
</div>
...
See, also, this short demo.

Resources