angular / angular.js

AngularJS - HTML enhanced for web apps!
https://angularjs.org
MIT License
58.79k stars 27.48k forks source link

ng-repeat with filters that introduce new collections #2302

Open jasonkuhrt opened 11 years ago

jasonkuhrt commented 11 years ago

There is a thorough discussion including analysis, ideas, and fiddles here (concluding with some successful work-arounds):

https://groups.google.com/forum/#!msg/angular/IEIQok-YkpU/oKuLvzCnAcoJ

A common instigating use-case is chunking arrays for grids:

http://stackoverflow.com/questions/11056819/how-would-i-use-angularjs-ng-repeat-with-twitter-bootstraps-scaffolding

I believe the issue can be summarized as difficulty using ng-repeat with filters that change the collection's reference...? Brainstorming possible directions forward:

  1. Would track by https://github.com/angular/angular.js/commit/61f2767ce65562257599649d9eaf9da08f321655 syntax make it possible to use filters that subsequently change the collection reference? It seems to me track by and this issue have nothing to do with each other ultimately.
  2. Could/should this issue by resolved by new additions to angular's destructing syntax? i.e.

    row in items chunk by 6

    Seems to me to be too specific a feature to make part of the destructing syntax.

  3. An angular $service that supports creating filters that modifier collection reference within ng-repeat. So that chunk or any other filter could be confidently applied to ng-repeat:

    What users are rolling on their own: http://jsfiddle.net/pkozlowski_opensource/zvpVg/2/

olefriis commented 11 years ago

Hi there! (I'm one of the guys from the Google Groups discussion...)

As I see it, the problem is that the ng-repeat directive (https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js) tracks changes to the given list in a quite complex way, assigning hash functions to the elements of the array, and not the way angular.equals (http://docs.angularjs.org/api/angular.equals) does it. As far as I can tell, angular.equals does a "deep compare" on arrays, and recursively so for arrays of arrays, etc.

When writing a chunk filter (my attempt: https://github.com/olefriis/simplepvr-backend-ruby/blob/master/public/js/filters.js) the most obvious way to do it is to... well, chunk the input array into a number of new arrays. This means that each time such a chunk filter is invoked, it will create new array instances, and ng-repeat will treat the output as completely new values each time.

If we could just make ng-repeat use angular.equals, all would be fine. I am aware that this may be slowing down execution for insanely large data sets, but having some way to switch on this behavior would be nice.

That's my 50 cent. Hope it's useful, and thanks for reading!

btford commented 11 years ago

As part of our effort to clean out old issues, this issue is being automatically closed since it has been inactivite for over two months.

Please try the newest versions of Angular (1.0.8 and 1.2.0-rc.1), and if the issue persists, comment below so we can discuss it.

Thanks!

jasonkuhrt commented 11 years ago

@btford the issue is outlined clearly above. I would appreciate at least a single comment from the Angular team rather than asking us to try a new version of angular and add yet more feedback.

Thanks

tiredenzo commented 11 years ago

Hello, I ran in the same issue to. the $$hashkey value is missing on newly created elements. the problem is well described in the google group post.

Thank you

btford commented 11 years ago

See track by in AngularJS 1.2.0. It should solve this issue.

olefriis commented 11 years ago

I tried using track by, but could not make it solve my problem. I wanted to create Twitter Bootstrap "row" divs with exactly 3 sub-divs in each, which is why I wrote my own chunk filter (which chunks one array into an array of arrays, each of a specified size). I cannot make this chunk filter work with AngularJS 1.2, and I did try using track by. I assume it's just me being too dumb, since the AngularJS developers are generally really awesome and very intelligent people.

However, in AngularJS 1.2 there's the ng-repeat-start and ng-repeat-end directives, and using these, I can periodically insert a "clearfix" div, which is good enough for me. See here for an example.

I might try a second time to make my chunk filter work in the weekend. If anybody has a jsfiddle or something showing how to do a chunk filter in AngularJS 1.2, I'll be very interested.

olefriis commented 11 years ago

So, I've fiddled a bit with track by and my own chunk filter. I have made a very simple example which shows that I'm too stupid to make it work :-) - github.com/olefriis/angularjs-chunk

I've read the docs on track by, and I think I get it, but I still cannot make it work.

olefriis commented 11 years ago

...and here's a JSFiddle in case you prefer that. http://jsfiddle.net/olefriis/cag9a/1/

When running the fiddle, the output is correct, but Angular throws a digest error in the JavaScript console.

kzar commented 11 years ago

I'm not sure that track by helps us with the problem because I think you have to use it before the filter. If we could do something like ng-repeat item in items | chunk:3 track by $index maybe it would work. (I might be wrong there!) My attempt http://plnkr.co/edit/Nyfa9bSOeBYBTfKmJ0oY?p=preview

Edit:

I have managed to get a hack working but I'm not sure how performant it'll be! (I serialise each chunk to JSON and then parse as I use it.) Here you go anyway http://plnkr.co/edit/XT3tJMlBKvIodYUkoVkO?p=preview

nizsheanez commented 11 years ago

but if you will filter $scope.$watch array like in your exercise, you will have: Uncaught Error: 10 $digest() iterations reached. Aborting!

nizsheanez commented 11 years ago

i use this pattern:

<div ng-repeat="item in array" ng-switch on="$index % 2">
   <div class="row" ng-switch-when="0">
       <div class="col-md-6" render-item-directive directive-var="array[$index+0]"></div>
       <div class="col-md-6" render-item-directive directive-var="array[$index+1]"></div>
   </div>
</div>
olefriis commented 11 years ago

That's a nice idea. However, in case you have an odd number of elements in your array, the last "array[$index+1]" will be "undefined", which you then need to handle in your directive.

nizsheanez commented 11 years ago

which way is better to handle it?

exocom commented 10 years ago

+1

dbillingham commented 10 years ago

Started using Angular today, this was the first issue I came across, wrote chuck function similar to above but angular seemed to be calling the function far more times than expected, and thus through errors. Having searched the web I'm surprised a solution has not been made available for what seems such a simple problem which could have be resolved in minutes using knockout.

grabus commented 10 years ago

if you want return new collection, you just need to save javascript links. I wrote factory for this case:

app.factory('linker', function () {
  var links = {}
  return function (arr, key) {
    var link = links[key] || []

    arr.forEach(function (newItem, index) {
      var oldItem = link[index]
      if (!angular.equals(oldItem, newItem))
        link[index] = newItem
    })

    link.length = arr.length

    return links[key] = link
  }
})

you can see here how it works: http://plnkr.co/edit/2Uc5zsFgVnK3ltHOUUQx?p=preview

OrganicPanda commented 10 years ago

I have a solution with a high-priority directive - is this clean? Feels better than some of the solutions posted so far:

http://jsbin.com/vumabivo/1/edit

bdkent commented 9 years ago

Was there ever any resolution to this issue? I just ran into this on angular 1.4.7...

frfancha commented 9 years ago

The "resolution" is quite simple: to display your items (length = n) in group of 3, just make your ng-repeat on 0..n/3 and display items[i * 3], items[i * 3 + 1] and items [i * 3 + 2] in each group

bdkent commented 9 years ago

I would call that a workaround, not a resolution.

To me, a resolution would:

For example:

<p ng-repeat="chunk in list | chunked:chunkSize">
    <span ng-repeat="x in chunk">{{x}}<span ng-if="!$last">,</span> </span>
</p>

$scope.chunkSize = 3;
$scope.list = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
frfancha commented 9 years ago

Angular works by dirty checking, so if the scope shows to the dirty checking code a new object at each loop, it just can't work. I would not call that a bug needing a resolution but just the way you must use angular.

wesleycho commented 8 years ago

IMO this probably shouldn't be supported in the framework itself. There are multiple ways around this now, and an API in core that a lot of people won't use is the wrong approach.

One can for example do

<div ng-repeat-start="item in items track by item.id">

</div>
<div ng-repeat-end
  ng-if="($index + 1) % 3">
  Display
</div>

Plunker of this in action here.

The benefit of this is that it allows more flexibility. If performance is an issue, then one should be already building an abstraction above/without ng-repeat anyway.