ng-repeat elements' indexes changing on sort - javascript

I have an HTML table with rows created using an ng-repeat and data using properties of objects in an object array. They are being sorted by a name column alphabetically. I have setup some simple editing using ng-show, ng-focus, and ng-blur, so that there is no need for a save button or something similar.
However, as I am editing the name column, if I type a first letter that would cause that row to be lower in the table, it sorts the table as I am still typing. I tried creating a "temp" object array, copying the original array into it, modifying the "temp" array when editing, then copying the temp array into the original array when editing is done. That didn't work; the same thing happened anyway.
After a little research, I learned that both the temp array and the original array were probably pointing to the same data, so I tried multiple ways of cloning the array. I finally got it to work...with one exception: when I edited the object to be sorted lower, it moved. When I tried to edit it again, it did all sorts of random, unexpected stuff.
After some diagnostics, I discovered that the indexes of the objects in the table (gotten from $index) were being changed when sorted. For example:
Table
-------------------------------------
|Name |Age |Phone |Index |
-------------------------------------
|George |25 |928-7495|0 |
|John |34 |342-0673|1 |
|Megan |28 |834-1943|2 |
|Susan |19 |274-8104|3 |
-------------------------------------
If I changed George to Tim, the index of that object becomes 3 also. All the other indexes stay the same. Can anyone tell me why this is happening and/or give me suggestions on how to fix this?

So apparently I just didn't realize that the indexes changed every time I changed the data or the sorting of the data. After adding the index as a property of my objects, everything worked as expected.

This is how I use the orderBy Angularjs filter with column sorts that reverse on click. The "a" tag in the "th" is what controls the behavior. I have this in a directive, which is recommended, but the code can be in a controller.
I use the Angularjs orderBy filter as they state.
$filter('orderBy')(array, expression, reverse)
// This is the order of properties in the code below that looks like this
// $scope.rows = angularSortBy( $scope.rows, columnName, toggleOrderDirection( columnName ));
Each column has a font-awesome multi-arrow toggle called
<i class="fa fa-sort"></i>
Where columns would be something like this...
$scope.columns = { columnOne: { label: 'Column One', orderDirection: true, selected: false },
columnTwo: { label: 'Column Two', orderDirection: true, selected: false },
columnThree: { label: 'Column Three', orderDirection: true, selected: true }};
and rows could be anything you wish...
<table>
<tr>
<th>Sort By</th>
<th ng-repeat="(key, value) in columns">
{{ value.label }}
<span ng-if="value.selected">
(<i class="fa fa-sort"></i>) // font-awesome arrow font.
</th>
</tr>
<tr ng-repeat="row in rows">// stuff here</tr>
var angularSortBy = $filter( 'orderBy' ); // use Angular's built in filter to sort table.
$scope.orderBy = function( columnName ){
resetAllSelectedStates(); // sets column.selected prop to false.
setAsSelected( columnName, true );
$scope.rows = angularSortBy( $scope.rows, columnName, toggleOrderDirection( columnName ));
}
function resetAllSelectedStates(){
Object.keys( $scope.columns ).forEach( resetColumnPropertyToDefault );
}
function resetColumnPropertyToDefault( name ){
$scope.columns[ name ].selected = false;
}
function setAsSelected( name ){
$scope.columns[ name ].selected = true;
}
function toggleOrderDirection( name ){
return $scope.columns[ name ].orderDirection = !$scope.columns[ name ].orderDirection;
}

Related

How to get a table row's index in vue-tables-2

I've inherited someone else's vue code that uses vue-tables-2. The table looks like this (very, very simplified)...
<v-client-table :data="myRows" :columns="columns" :options="options">
<div slot="columnA" slot-scope="{row}">
<input type="text" v-model.trim="row.someKey" class="form-control">
</div>
<div slot="etc"> ...
Notice how it says slot-scope="{row}", instead of slot-scope="row". As I inspect rows, I find each row looks like this...
{ row: { keyA: value, keyB: value, etc }, index: 1 },
{ row: { keyA: value, keyB: value, etc }, index: 2 }, etc
So I guess the original author "destructured" the row to avoid wasting keystrokes, as in v-model.trim="row.row.someKey".
Now I'm adding a new column which uses the row and the array index of the row data, like this...
<div slot="columnZ" slot-scope="row">
<p>col value is {{row.row.someOtherKey}} and index is {{row.index-1}}<p>
</div>
But I don't like row.row any better than the original author, and the row.index is given in counting numbers, not the zero-based array index.
I assume vue-tables-2 is the source of both of these issues. Is there a way I can use the package but just get the row data without having it wrapped in another "row", and is there a way I can get a zero-based array index per row?
Is there a way I can use the package but just get the row data without having it wrapped in another "row"
Yes and no. Slot props (anything the component shares with it's slot) is always shared as a singe object. You are the one who gives the variable the name.
So in case of slot="columnZ" slot-scope="row" (which is by the way a deprecated syntax - current syntax is v-slot:columnZ="row")
You can chose any other name - instead of row it can be props or anything else
what you get is whole object - { row: { keyA: value, keyB: value, etc }, index: 1 } (assuming)
But you can destructure the object into components - v-slot:columnZ="{ row, index }"
This is same operation as in plain JS:
const obj = { row: { keyA: 1, keyB: "asdasd", }, index: 1 }
// here the names `row` and `index` are given by the names of 1st level properties
const { row, index } = obj
console.log(row)
console.log(index)
// it is possible to "rename" te variable
const { row: user } = obj
console.log(user)
As for the missing zero-based index - if this is what the component shares with the slot, there is very little you can do about it. The component is in control. If you really need it, just add one more key into every row containing zero-based index

AngularJS forEach set object properties in ng-repeat found set

I have an ng-repeat displaying items from json, based on what the user enters into a field it can be filtered.
<tr ng-repeat="i in filteredItems = (iso3166 | filter: {alpha_2: isoQuery})">
this all works as expected.
Each item in the group ("iso3166") has 3 boolean values, "restricted", "unrestricted", and "unclassified", for each of the columns "license", "prohibited", and "size".
The user can set one of these values in each column in each row to true or false:
This works fine.
What I need to add is an "ALL" row at the top that will set every item in the filtered set to whichever button was clicked in each column.
In other words, if the user clicks the ALL rows "restricted" button in the "license" column, every row's "license" "restricted" value should be toggled to true or false. I'm not sure of the syntax to set the value.
Now I have the ng-click as
ng-click="setAll('col1','licensed')"
and the function as
$scope.setAll = function (column, value) {
angular.forEach($scope.filteredItems, function (value, key) {
??
});
};
How do I assign the correct value to each row in the loop?
At the moment your inner function shadows the value variable. Rename the local variable to something else in order to change it.
$scope.setAll = function (column, value) {
angular.forEach($scope.filteredItems, function (item, key) {
item[column] = value;
});
};

Filtering data with AngularJS Filter. How to iterate over objects?

This is my Filter:
app.filter('ticketsFilter', function (){
return function (input){
var data = input;
console.log("Filtered object:")
console.log(data);
return input;
};
});
This is my Markup:
<tr ng-repeat="ticket in tickets | ticketsFilter">
<p> {{ticket.createdByName}} </p>
<p> {{ticket.title}} </p>
<p> {{ticket.toDpt}} </p>
</tr>
The tickets Object is a Object of Objects. On my DB, there are 4 instances of tickets. And, when my ticket hits the filter, the Filtered Object is displayed as:
[Object, Object, Object, Object]
I could not figure out how to acess these objects. I've tried to use $index, received a dependency injection. Tried to use a (for x in input) loop, but the console.log will only display the last object properties. I've tried for(i, i
I can properly acess my objects properties using this:
console.log(data[0].attribute);
But I dont know how to iterate and acess data[i] instead of data[0]. Well, any tips on how to acess my Object Objects index? I can give more code if needed.
Thanks for any advice.
#EDIT
This is how my console.log($scope.tickets) looks like:
fromDpt: Array[4]
0: Object
1: Object
2: Object
3: Object
Every Object looks like this:
0: Object
__v: 0
_id: "55ddb1e6ca11301020ef9b77"
createdAt: "1440592358061"
createdById: "55d758ee3a7ff720141a18f8"
__proto__: Object
The code below will apply the filter on all objects within tickets and returning the complete array of objects:
<tr ng-repeat="ticket in tickets | filter:ticketsFilter">
If you want to use the filter to alter the output of a specific item, you can apply the filter one level below the ng-repeat:
<tr ng-repeat="ticket in tickets">
{{ticket.name | filter:ticketsFilter}}
</tr>
But it depends on what you want your filter to do. I suggest you to also take look at some info how to apply filters: https://docs.angularjs.org/api/ng/filter/filter
If this does not work for you I would suggest you to be a bit more specific on what you want your filter to do.
using the Ng-model and pasing the model as a filter...
here an exaple: http://jsfiddle.net/leojavier/njm96Luv/2/
<div class="field" ng-app="App">
<input type="text" ng-model="model">
<table ng-controller="Controller">
<tr ng-repeat="item in table | filter:model">
<td ng-class="item.style"><i ng-class="item.badge"></i> {{item.name}}</td>
</tr>
</table>
</div>
js
var App = angular.module('App', []);
App.controller('Controller', function($scope, $timeout){
$scope.table = [
{name : 'Dan',
badge: 'fa fa-futbol-o'
},
{name : 'Orlando',
badge: 'fa fa-bicycle'},
{name : 'Dany',
badge: 'fa fa-bolt'}
];
$scope.delete = function(item, index){
item.deleted = true;
item.style = 'deleted';
function destroy() {
$scope.table.splice(index, 1)
}
$timeout(function(){destroy();}, 3000);
}
})
exaple: http://jsfiddle.net/leojavier/njm96Luv/2/
below div will display the ticket as per my textbox input
<input type="text" ng-model="tickets.Id">
<div ng-repeat="ticket in tickets | filter:tickets">
I think what you're trying to do is nest the ng-repeats. Correct me if I'm wrong, but if I'm right you can do something like this:
<div ng-repeat="ticket in tickets">
<div ng-repeat="myOtherObject in ticket.someProperty | filter:something">
</div>
</div>
Sorry for the sloppy code, on mobile, but you should get the idea.
All of you gave me great insight about this problem. I thank you every and each of you. Since none had a full answer, I'm going to summarize all the tips and create a full answer to this problem:
First of all. When I'm trying to iterate over
<tr ng-repeat="ticket in tickets | ticketsFilter">
the ticketsFilter will receive a instance of ticketS. TicketS is an Object containing multiple ticket Objects, and each ticket having some properties. So:
$scope.tickets = {
ticket: { name: 'Philip', status: 'good' },
ticket: { name: 'R2B21', status: 'sad' },
ticket: { name: '42', status: 'good' },
ticket: { name: 'MVW', status: 'bad' }
};
Knowing that my filter will receive tickets as input, I could reach every ticket inside of it with a proper for loop. This way:
app.filter('ticketsFilter', function () {
return function(tickets){
for (var i = 0; i < tickets.length; i ++) { ...
Inside of the for loop, I will reach each and every ticket contained in tickets. So, I just need to grab them and check if they have the propertie that I want. I this case, I want to return only tickets that have a status of 'good'. So:
var ticket = tickets[i]; //Grabing the ticket inside the for loop.
if (ticket.status == 'good') { //Checking if my ticket is good.
Now, I need two things. First, I need to return my data and I need to push every filtered 'good' ticket into my returned data. I need to remember, tought, that the returned object must not be 1 ticket, but ticketS. Because of that, I'm going to create a empty array called filtered before the for loop. And, to push use my if to insert ticket[i] that have a propertie of status=='good'.
So, the full code will be:
app.filter('ticketsFilter', function (){
return function (tickets){
//creating the empty object that will return data
var data = [];
for (var i = 0; i < tickets.length; i ++) {
var ticket = tickets[i];
if (ticket.status == 'good'){
//Inserting 'good' ticket inside of data
data.push(ticket);
};
};
//And, finally, returning the data
return data;
};
});
After that, If I have the Markup:
<tr ng-repeat="ticket in tickets | ticketsFilter">
<p><b> {{ticket.name}} </b></p>
</tr>
It would display:
Philip
42
And that is it, iterating over an array of objects and checking their properties. I want to thank, again, every one that tried to help and want to sorry about my bad explanation.
Thank you!

AngularJs why is object undefined on ng-disabled function?

VIEW:
I have a rows repeating , with a save button on each row to save each object individually. I want this button to be disabled if no changes have been made.
<tr ng-repeat="option in options | filter:search">
<a ng-click="save(option)" ng-disabled="isUnchanged(option)">Save</a>
</tr>
CONTOLLER:
So I pass the option object to the function, I get its index position in the array. Then compare this 'option' object to its original self in apiKeyOptions[index] which is injected as a service.
angular.module('PartOfApp')
.controller('PartOfAppCtrl', function( $scope, ... apiKeyOptions) {
$scope.options = apiKeyOptions;
$scope.isUnchanged = function(option) {
var index = $scope.options.indexOf(option);
//compare object to the original
if(option.value == apiKeyOptions[index].value && apiKeyOptions[index].setting == option.setting){
//then no changes have been made to this
return true;
}else{
return false;
}
For some reason I get a console of 100's of errors when any data is changed, saying that the apiKeyOptions[index].value and apiKeyOptions[index].setting are undefind.
The app works perfectly as it should returning true if they are the same but still throws a
TypeError: Cannot read property 'value' of undefined
on apiKeyOptions[index]
if I console.log(apiKeyOptions[index].value) I get no undefined values and all log correctly.
Im guessing Im breaking some angular rules, if anyone could help that would be great.
apiKeyOptions overview:
apiKeyOptions is an array of up to 50 objects
each object is in the form
{
defaultValue: boolean,
description: null,
name: String,
setting: "Default" or Boolean,
value: Boolean
}
Added after comment below:
If I add-
console.log(index);
console.log(apiKeyOptions[index]);
to the function $scope.isUnchanged, I get the expected results
example :
13
Object {name: "LOREM IPSUM", description: null, defaultValue: false, setting: "default", value: falseā€¦}
So index is not always -1. The reason I pass the object to the function and not $index is because of the filter | search so the index will change depending on the search.
FIXED
As shown in the answer below . I was getting a index = -1 error but its was buried in 100's of CORRECT log outputs.
Oddly this did not stop the app from working and I will need to have a deeper look into how ng-disabled is bound to a value. To fix I simply replaced the indexOf with
for (var i = 0; i< $scope.options.length; i++ ){
if($scope.options[i].name == option.name){
var index = i;
}
}
The problem seems to be with the parameter passed to $scope.isUnchanged = function(option) {
Since ng-repeat creates a new scope for each loop, i suspect that the 'option' available to each loop would be a new object and will not have a reference to 'options' array.
<tr ng-repeat="option in options | filter:search">
Therefore your isUnchanged function will receive parameter as a new object and hence below code always returns -1. Because indexOf matches the given argument in the array and since the argument 'option' is an object and doesn't refer(reference comparison) the same element of array hence no match will found. i.e var a = {id:1};var b = [a]; b.indexOf({id:1}) === -1; b.indexOf(a) === 0;
var index = $scope.options.indexOf(option);//always be -1 in your case
// therefore apiKeyOptions[index] will always be undefined
As a workaround you should pass $index to isUnchanged from the view.

With an angularjs ng-repeat, can I force one element to be first in the list?

I have an array, keywords, printing with an ng-repeat:
<li ng-repeat="keyword in keywords"> {{ keyword }} </li>
Which when it's sorted alphabetically would display, e.g:
Apples
Cucumbers
Daikon
Turnip
I want that when a user searches a specific keyword, that keyword gets "pinned" to the top of the list, no matter how else the list is sorted. So if the user searches "Turnip", Turnip is first in the list, while the rest remains sorted alphabetically:
Turnip
Apples
Cucumbers
Daikon
I am wondering if this functionality is possible with ng-repeat, or if I will need to construct it by inserting an additional element at the top and then filtering just that one from the array.
I'm adding another answer, as I think both could be used, but this one with sorting is much slicker!
Here, I just do a sort of the array of objs on the pinned first then on the name value as you wanted it:
<li ng-repeat="obj in array | orderBy:['pinned','name']:reverseSort ">{{ obj.name }} [<label>Pin</label><input type="checkbox" ng-model="obj.pinned" ng-click="pinObj(obj)" />]</li>
http://plnkr.co/edit/8NGW3b?p=info
Cheers
You can create a custom angular filter that handles the sorting. Then you could just use
<li ng-repeat="keyword in keywords|my_sort"> {{ keyword }} </li>
http://docs.angularjs.org/guide/filter
good luck!
I could imagine that you could have instead of just a key in your array, you could have an array of objects for example:
array {
[ key: "Turnip",
pinned: true],
[ key: "Apples",
pinned: false] }
And then, in your ng-repeat, then you could have a filter that splits out the pinned versus unpinned as required.
app.filter('pinned', function() {
return function (list, pinned, scope) {
var test = (pinned == 'true' ? true : false);
var returnArray = [];
for (var i = 0; i < list.length; i++) {
if (list[i].pinned === test) {
returnArray.push(list[i]);
}
}
return returnArray;
};
});
I've created this plunk to show what I mean above. A potentially slicker solution would be to sort your array by the pinned attribute.
http://plnkr.co/edit/onFG7K61gLLqX31CgnPi?p=preview

Categories