Angular ng-repeat not updating after splice - angularjs

I am working on a Angular Material site and using ng-repeat to populate a table. The list generated by the ng-repeat contains a list of users (the users array is populated by a RESTFul query).
Each "item-content" of the ng-repeat has a "Delete" button to delete/splice the item/user out of the array.
If I have no filters applied to the ng-repeat statement...when I splice the index of the selected user out of the array....the UI/DOM is updated and the user is removed from the list.
If I apply a filter (filters by the first letter of the last name and only shows those results)...when I splice the index out of the array...the UI/DOM is NOT updated and user remains in the list (from what I can tell from walking through the delete function from the console...the array is updated and the index/user is removed).
I filter being used is a custom angular filter and takes one input (the starting letter of the last name you want to filter on).
app.filter('startsWith', function () {
return function (items, letterMatch) {
var re = new RegExp("^[" + letterMatch.toUpperCase() + letterMatch.toLowerCase() + "]$");
var filtered = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
var lastNameLetter = item.user.name.substr(item.user.name.indexOf(' ') + 1, 1);
if (re.test(lastNameLetter)) {
filtered.push(item);
}
}
return filtered;
};
});
Here is the ng-repeat statement:
<md-item ng-repeat="user in ddUsers | startsWith:selectedFilter | startFrom:currentPage*pageSize | limitTo:pageSize">
In the controller...I am using Dialog Prompt (part of the Angular Material 1.1 RC framework) to initiate/confirm the Delete and proceed with the splice.
$scope.showPrompt = function(ev,index,user) {
// Appending dialog to document.body to cover sidenav in docs app
var confirm = $mdDialog.prompt()
.title('Delete User?')
.textContent('Type in DELETE to confirm deletion of the user.')
.placeholder('DELETE')
.ariaLabel('Delete User')
.targetEvent(ev)
.ok('Delete')
.cancel('Cancel');
$mdDialog.show(confirm).then(function(result) {
if(result=="DELETE")
{
$scope.ddUsers.splice(index, 1);
$scope.showSimpleToast('Successfully deleted ' + user.user.name);
}
else
{
$scope.showSimpleToast('Did not confirm properly');
}
}, function() {
$scope.showSimpleToast('Cancelled deletion for ' + user.user.name);
//var message = $scope.filterLastName(user.user.name);
//$scope.showSimpleToast(message);
});};
When the controller loads...I have a init function that executes and populates ddUsers from a custom angular factory:
var init = function () {
$scope.ddUsers = $UserList.UserList;
}
init();
I am not sure why ng-repeat is not updating after the splice when a filter is applied. If I have no filter on the ng-repeat, ng-repeat does update the UI/DOM to reflect the change and you don't see the user anymore.

While it is not shown how you call the function I assume you are passing in $index from view.
The problem with doing that is that $index will not be the same index for filtered array as for the original array so you would be splicing the wrong element out of the main data array
You need to do your own indexing by passing in the actual object.
I will also assume that user is that object so you would do:
if(result=="DELETE")
{
var idx = $scope.ddUsers.indexOf(user);
if(idx > -1 ){// make sure can index this object
$scope.ddUsers.splice(idx, 1);
$scope.showSimpleToast('Successfully deleted ' + user.user.name);
else{
// Ooops ... can't find it
}
}

Related

ui grid returning rows in the same order they are selected?

Hi am using UIgrid in an angularjs project and when I call the method gridApi.selection.getSelectedRows() to get the selected rows it returns an array with all the rows but in a random order. Ideally I want to get the rows in the same order they were selected (as if gridApi.selection.getSelectedRows() is backed by a queue) . Any idea how to achieve this please ?
This link to plunker shows the issue http://plnkr.co/edit/gD4hiEO2vFGXiTlyQCix?p=preview
You can implement the queue by yourself. Something like
$scope.gridOnRegisterApi = function(gridApi) {
gridApi.selection.on.rowSelectionChanged($scope, function(row) {
var selections =gridApi.selection.getSelectedRows();
// add sorted
selections.forEach(function(s){
if ($scope.mySelections.indexOf(s) === -1) {
$scope.mySelections.push(s);
}
});
// remove the ones that are not selected (use for to modify collection while iterating)
for (var i = $scope.mySelections.length; i >0; i--) {
if (selections.indexOf($scope.mySelections[i]) === -1) {
$scope.mySelections.splice(i, 1);
}
}
console.log($scope.mySelections);
row.entity.firstSelection = false;
if (row.isSelected) row.entity.firstSelection = (gridApi.selection.getSelectedCount() == 1);
});
};
I think there is a bug with that version of angular I was using there , if you upgrade the version in the plnkr to 1.6.1 it will behave as expected

angular filter on object works but causes Infinite $digest Loop

I gave an object as followed
{
key1: [{...}, {...} ....],
key2: [{...}, {...} ....],
.........so on .....
}
I have an ng-repeat ng-repeat="(key, values) in data" and then inside that ng-repeat="val in values"
I want to set up an filter based on some property of objects stored in the array. I have set up below filter
.filter('objFilter', function () {
return function (input, search,field) {
if (!input || !search || !field)
return input;
var expected = ('' + search).toLowerCase();
var result = {};
angular.forEach(input, function (value, key) {
result[key] = [];
if(value && value.length !== undefined){
for(var i=0; i<value.length;i++){
var ip = value[i];
var actual = ('' + ip[field]).toLowerCase();
if (actual.indexOf(expected) !== -1) {
result[key].push(value[i]);
}
}
}
});
console.log(result);
return result;
};
The filter seems to work fine when I use ng-repeat="(date, values) in data| objFilter:search:'url'" but for some reason it is called too many times and causes Infinite $digest Loop.
Any solutions??
Edit:
I have created below plunker to show the issue. The filter works but look in the console for the errors. http://plnkr.co/edit/BXyi75kXT5gkK4E3F5PI
Your filter causes an infinite $digest loop because it always returns a new object instance. With the same parameters it returns a new object instance (it doesn't matter if the data inside the object is the same as before).
Something causes a second digest phase. I'm guessing it's the nested ng-repeat. Angular calls filters on every digest phase and because you filter returns a new value it causes the framework to reevaluate the whole outer ng-repeat block which causes the same on the inner ng-repeat block.
Option 1 - modify the filter
One fix you can do is to "stabilize" the filter. If it's called 2 times in a row with the same value it should return the same result.
Replace your filter with the following code:
app.filter('objFilter', function () {
var lastSearch = null;
var lastField = null;
var lastResult = null;
return function (input, search, field) {
if (!input || !search || !field) {
return input;
}
if (search == lastSearch && field == lastField) {
return lastResult;
}
var expected = ('' + search).toLowerCase();
var result = {};
angular.forEach(input, function (value, key) {
result[key] = [];
if(value && value.length !== undefined){
for(var i=0; i<value.length;i++){
var ip = value[i];
var actual = ('' + ip[field]).toLowerCase();
if (actual.indexOf(expected) !== -1) {
//if(result[key]){
result[key].push(value[i]);
//}else{
// result[key] = [value[i]];
//}
}
}
}
});
// Cache params and last result
lastSearch = search;
lastField = field;
lastResult = result;
return result;
};
});
This code will work but it's bad and prone to errors. It might also cause memory leaks by keeping the last provided arguments in memory.
Option 2 - move the filter on model change
Better approach will be to remember the updated filtered data on model change. Keep you JavaScript as is. Change only the html:
<body ng-controller="MainCtrl">
<div ng-if="data">
Search:
<input type="text"
ng-model="search"
ng-init="filteredData = (data | objFilter:search:'url')"
ng-change="filteredData = (data | objFilter:search:'url')">
<div ng-repeat="(date, values) in filteredData">
<div style="margin-top:30px;">{{date}}</div>
<hr/>
<div ng-repeat="val in values" class="item">
<div class="h-url">{{val.url}}</div>
</div>
</div>
</div>
</body>
First we add a wrapper ng-if with a requirement that data must have a value. This will ensure that our ng-init will have "data" in order to set the initial filteredData value.
We also change the outer ng-repeat to use filteredData instead of data. Then we update filtered data on the model change with the ng-change directive.
ng-init will fire once after data value is set
ng-change will be executed only when the user changes the input value
Now, no matter how many consecutive $digest phases you'll have, the filter won't fire again. It's attached on initialization (ng-init) and on user interaction (ng-change).
Notes
Filters fire on every digest phase. As a general rule try avoiding attaching complex filters directly on ng-repeat.
Every user interaction with a field that has ng-model causes a $digest phase
Every call of $timeout causes a $digest phase (by default).
Every time you load something with $http a digest phase will begin.
All those will cause the ng-repeat with attached filter to reevaluate, thus resulting in child scopes creation/destruction and DOM elements manipulations which is heavy. It might not lead to infinite $digest loop but will kill your app performance.

angular $index numbering with condition in ng-repeat & ui-sortable

I'm trying to achieve customized numbering while listing all items in an array.
All items in array are rendered using ng-repeat & ui.sortable.
Numbering must be done in such a way that, for an array item "statement", count should not be increased & displayed.
(Else I may be used $index instead of an external count.)
For any other array item, count should be increased & displayed.
The solution that got me the the closest result was the one where I passed $index into a filter function written in the controller.
like in HTML:
<li ng-repeat="question in questions">
<div class="no"> {{ filterIndex(question.question, $index) }} </div>
<div>{{question.question}}</div>
</li>
in controller:
var filterValue = 0;
$scope.filterIndex = function (value, count) {
if (count === 0) {
filterValue = 0;
}
if (value !== 'statementText') {
filterValue = filterValue + 1;
return filterValue;
}
else {
return '"';
}
};
Even it was working without any errors, the count returned from function is not get updated like we get with $index when we update the order using ui-sortable.
see it here: js-fiddle using filter function
means, once it rendered (4) in <[ (4) fourth question ]> will remain same even if we moved it to top or bottom by dragging.
I tried different ways and almost everything ended up on 'Maximum iteration limit exceeded.'.
Real scenario is a little bit complex as it contains nested ng-repeats and similar counting with digits and numbers alternatively in child loops.
Here is link to start fresh:
js-fiddle
You can inject this to your controller, it will listen for array changes, and update the indexes:
var update = function(){
var currentCount = 0;
var questions = $scope.questions;
for(var j = 0; j < questions.length; j++){
if(questions[j].question != null){
if(questions[j].question.indexOf("statement") < 0){
questions[j].calculatedIndex = ++currentCount;
}
}
}
};
$scope.$watchCollection('questions', function(){
update();
});
update();
probably needs a bit fine-tuning as I didn't concentrate on your question completely. In the ng-repeat, you now have access to calculatedIndex. It will be "NULL" for statement items, you can use ng-show for that.
just try to add extra option:
ng-init='question.index = $index+1'
http://jsfiddle.net/ny279bry/

In an ng-repeat, is the iterator offset of $index dynamic to the results of a filter

In an ng-repeat, is the iterator offset of $index dynamic to what is visible? I am getting seemingly incorrect $index values when a filter is applied.
Working with no filter applied:
Not appearing to work with filter applied (Note the console log):
When a filter is removed:
And finally my ng-click call:
<a ng-click="showHideOrderDropDown($index)" href="">
Show More<br/><i class="icon-arrow-down"></i>
</a>
Click handler:
$scope.showHideOrderDropDown = function(index) {
console.log(index);
$scope.data[index].orderDropDown = !$scope.data[index].orderDropDown;
};
Now I can easily work around this, but I was just hoping for some clarification.
After doing some research it appears that applying a Filter actually adds and removes (not hides) elements from the ng-repeat therefore the $index would apply to the new order of the array and no longer reflect the $scope array object.
Since asking the question I went ahead and passed the database id to the controller instead.
$scope.showHideOrderDropDown = function(id) {
for (var i = 0; i < $scope.data.length; i++) {
if ($scope.data[i].id === id) {
$scope.data[i].orderDropDown = !$scope.data[i].orderDropDown;
}
}
};

Remove item from list after filtering

I have the following issue:
I've create a list that allow the user to delete an item from list, as following:
When user click on trash icon, the item is removed normally.
The problem is when the user uses the filter on top.
In that case, if I delete the number 6565 (index 4 in original list, 1 on filtered list), the item deleted is on index 1 on original list, resulting on delete the register with number #564456
This is my delete call on click:
$scope.deleteOwn = function (uuid) {
console.log(uuid);
var coupon = $scope.ownsCoupons[uuid];
Coupon.delete({'id' : coupon.uuid}, function () {
$scope.ownsCoupons.splice(uuid, 1);
});
}
And this is my html template:
<td></i></td>
I also try to use the code: $scope.ownsCoupons.splice(coupon, 1);without success.
Does anyone know how to fix that?
I've coded using the following reference: AngularJS How to remove an Item from scope
[EDIT]
I've created a Plunker to this: http://plnkr.co/edit/Fhxp6uZyTJCY05CAQ7yA?p=preview
As mentioned by #pkozlowski.opensource, you can't depend on $index to identify an item in an array in this way. I would make the following changes:
HTML:
<td><a ng-click="deleteWish(coupon)"><i class="icon-trash"></i></a></td>
JS:
$scope.deleteWish = function (coupon) {
var index = $scope.coupons.indexOf(coupon);
if (index != -1) {
$scope.coupons.splice(index, 1);
}
}
Here is a working Plunker: http://plnkr.co/edit/b0b2cYGsM5wtw8hIrQB5?p=preview

Resources