5

Using the random orderBy sort technique in this question works fine in AngularJS 1.1.

var myApp = angular.module('myApp',[]);

function MyCtrl($scope) {
    $scope.list = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
    $scope.random = function() {
        return 0.5 - Math.random();
    }
}

In 1.2, though, it puts infdig errors into the console and takes a much longer time to return the sorted results: http://jsfiddle.net/mblase75/jVs27/

The error in the console looks like:

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [["fn: $watchCollectionWatch; newVal: 42; oldVal: 36"],["fn: $watchCollectionWatch; newVal: 47; oldVal: 42"],["fn: $watchCollectionWatch; newVal: 54; oldVal: 47"],["fn: $watchCollectionWatch; newVal: 61; oldVal: 54"],["fn: $watchCollectionWatch; newVal: 68; oldVal: 61"]]

The documentation for orderBy doesn't have an example of using function expressions, only string expressions. Did something change, or is this a bug?

Community
  • 1
  • 1
Blazemonger
  • 90,923
  • 26
  • 142
  • 180
  • Update: an [error submission](https://github.com/angular/angular.js/issues/6133#issuecomment-34229877) resulted in the explanation of what was happening internally. – Blazemonger Feb 05 '14 at 20:19

2 Answers2

9

I'm not sure about previous versions, but in the current version, any expression watched on a scope, such as that passed to ng-repeat is usually evaluated at least twice per digest. The digest cycle only finishes when the results of all evaluated expressions, across all scopes of the entire Angular app, are identical between two successive evaluations.

Because each evaluation of

<li ng-repeat="i in list | orderBy:random">{{i}}</li>

results in calls to random(), and so a different order, then Angular will keep on evaluating the expressions, until it hits its limit of 10 digest iterations, and throws an error.

The solution to this is to set the order outside of the template, in the controller:

$scope.list = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
$scope.rankedList = [];
angular.forEach($scope.list, function(item) {
    $scope.rankedList.push({
        item: item,
        rank: 0.5 - $window.Math.random()
    });
});

And then order using the field by something like:

<li ng-repeat="i in rankedList | orderBy:'rank'">{{i.item}}</li>

This can be seen at this jsfiddle .

Michal Charemza
  • 25,940
  • 14
  • 98
  • 165
  • Good answer -- I took it one step further by putting that 'random rank' generator in a scoped function, so I can trigger it on demand in my application via `ng-click`. – Blazemonger Feb 05 '14 at 20:20
  • I simplified this by just adding a `rank` value to each item in the existing array rather than creating a new 'ranked' array. Thanks for the idea, – David Jun 08 '15 at 03:06
2

You can solve this in an Angular way with a simple custom filter. Here I'm using the underscore shuffle method which implements Fischer-Yates.

You could substitute the guts of the shuffle with your own algorithm if you prefer.

angular.module('shuffle', [])
  .filter('shuffle', function() {
    return function(ary) {
      return _.shuffle(ary);
    }
  });

We can now pipe our array through this filter, like so:

<li ng-repeat='option in options | shuffle'>

The filter will be called once when the template is rendered.

superluminary
  • 47,086
  • 25
  • 151
  • 148
  • I'm not using Underscore. – Blazemonger Nov 13 '14 at 20:04
  • You can substitute lodash. – superluminary Nov 13 '14 at 20:07
  • 1
    You could write your own shuffle function if you like, but since shuffling is a solved problem I think this is a good time to use a library. – superluminary Nov 13 '14 at 22:02
  • 1
    I downvoted as I felt the solution should be possible without any external libraries and that including a library just for a single randomization was overkill. On reflection that wasn't part of the actual question and was more related to the issue that I was facing. If an when you make any edits comment here and I'll cancel out my downvote with an upvote. – David Jun 08 '15 at 14:58
  • 1
    Hi @DavidAnderton I wanted to show that the correct place to do a shuffle is in a filter. Filters should be used for munging data. The actual mechanism by which the shuffle is achieved seems to me to be JavaScript question and beyond the scope as it were. – superluminary Jun 08 '15 at 15:54
  • I didnt get that from your answer that a filter was the proper way to go about this. Could you add why filters should be used instead of orderBy (which angular newbies, as myself) are naturally drawn to for this type of issue – David Jun 08 '15 at 15:58