11
$scope.clearCompleted = function()
        {
            angular.forEach($scope.todos, function(todo, i)
            {
                if(todo.done)
                {
                    $scope.todos.splice(i, 1);
                }
            });

            if($scope.todos.length == 0)
            {
                $scope.isEmpty = true;
            }
        }

This is my code to delete the 'done' todos from an array, but when two todos after each other are removed, it only removes the second. I think it's because the splice function resets and the returns the spliced array.

1
  • Yes, .splice() mutates an Array. This needs to be accounted for if using a forward iteration. Commented Jun 2, 2013 at 16:05

5 Answers 5

19

You splice elements from the array, which you iterated, therefore indexes in "todos" reduced. Sorry for my bad english.

var notDonedTodos = [];
angular.forEach($scope.todos, function(todo, i)
{
    if(!todo.done)
    {
       notDonedTodos.push(todo);
    }
});

$scope.todos = notDonedTodos;
Sign up to request clarification or add additional context in comments.

Comments

19

This is happening because forEach only knows about the initial state of the array, and therefore calls your method twice, even if the first call removes an item from the array. Just do a simple while loop instead:

var i = $scope.todos.length;
while (i--){
    if ($scope.todos[i].done){
        $scope.todos.splice(i, 1);
    }
}

1 Comment

It's worth pointing out that the trick in this solution is that the array is being processed in reverse order, allowing us to ignore the volatility of the array's length. This is equally valid with a traditional for loop as well.
7

The alternative i've found myself is to use the array.filter method. It is the most easy way to filter an array based on object keys. If you're working on an IE8 project (poor you) you'll need to add a polyfill for this function since it's fairly new to JavaScript.

Everything you need to know about javascript.

Answer code:

$scope.clearCompleted = function() {
    $scope.todos = $scope.todos.filter(function(item) {
        return !item.done;
    });
}

Comments

3

The issue with the each iteration is that it removes an item from the array causing an iteration to be skipped. jQuery has a nice grep method that returns all elements matching a certain criteria that is determined by a provided anonymous function.

var todos =[{id:1, done:false},{id:2, done:true},{id:3, done:true}];

function removeCompleted(todos){
    return $.grep(todos,function(todo){
        return todo.done == false;
    });
}

todos = removeCompleted(todos);
console.log(todos);

Working Example http://jsfiddle.net/ktCEN/

Documentation

Comments

2

As another alternative, you could just decrement your index each time you do a splice. For instance:

$scope.clearCompleted = function() {
    angular.forEach($scope.todos, function(todo, i) {
        if(todo.done) {
            $scope.todos.splice(i, 1);
            i--;
        };
    });

    if($scope.todos.length == 0) {
        $scope.isEmpty = true;
    };
}

This adjusts your index to maintain its validity each time your array is modified. You can still use angular.forEach, and you don't end up with two copies of your array.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.