-
Notifications
You must be signed in to change notification settings - Fork 27.5k
ng-repeat : Fix issues when iterating over primitives #1661
Conversation
@petebacondarwin amazing. I hope @IgorMinar and/or @mhevery get on board with this as refactoring out the $whatChanged service will be very helpful for directives to act on changes to an underlying object/array without needing to create useless DOM in an ng-repeat to fake this behaviour. +1 |
Works wrong with keys when elements removed from an array: <div ng-repeat="(k, v) in mylist">
<div class="input-prepend">
<button class="btn" ng-click="RemoveElement(k)">X</button>
<input type="text" ng-model="mylist[k]"/>
</div>
</div> $scope.RemoveElement = function(idx) {
$scope.mylist.splice(idx, 1);
}; |
@diseaz Thanks for this catch. Could you provide a working Plunker or unit test that demonstrates this issue? I am not sure I follow exactly what is the problem. |
I cloned and built this locally, and it fixed issues I was having related to #1267. Awesome! |
@petebacondarwin error demo: http://embed.plnkr.co/gZy4HT/preview Try removing elements and look at keys and $indexes (especially at the last one). |
@diseaz. Thanks for looking into this and providing a very comprehensive demo. It is great to have more eyes on such a major reimplementation. So what you are doing is using the syntax for iterating over an object's fields (i.e. If you are iterating over an array you should use the other syntax (i.e. I am pretty sure that what you are doing is not and should not be supported. @mhevery & @IgorMinar : Can you confirm if this is the case? If it is not supported then I would suggest that we thrown an exception if an array is passed as source but with the iterate over object syntax. While this change does not break the current unit tests it will slow down ng-repeat updates where objects are simply being moved around an array. I would be interested to here from the core team on this before adding this change. Also, @diseaz, we would need some unit tests that demonstrate the "bug" before the "fix" could be included. |
@petebacondarwin I've committed unittests into the same branch: https://github.com/diseaz/angular.js/tree/t-repeat Everyone who writes in Javascript knows for (var i in collection) {
doSomethingWith(collection[i]);
} works absolutely the same whether collection is an array or an object. Why ngRepeat should break this expected similarity? And even more: breaking support for keys while iterating arrays will be a regression. 1.x.y vesions support this behavior, and my unit tests pass on master and v1.0.x branches. |
There are other places where javascript syntax needs to be altered slightly to work as a directive argument. For instance, |
@petebacondarwin it's about performance, not about logical difference. |
Hi Dmitry I am not sure what you mean by performance over logic. The thing is that objects and arrays do behave differently when you manipulate them, which is where the issue you raise lies, and what I was getting at with the reference to the Stack Overflow question. For a start, even in your example, you can't treat the dictionary and the array the same, you have different functions for removing items, since objects don't have splice. If you were consistent then you would use delete on both and of course this doesn't work as expected: http://plnkr.co/edit/ZCNkyL?p=preview. Also, adding items is to an array is different from adding items to an object. Take this example using AngularJS 1.1.1: http://plnkr.co/edit/dURr0qrQSCTjjNActj8F?p=preview. Pete |
@petebacondarwin yes, arrays and objects behave differently, but iteration has the same wording. ngRepeat is about iteration, not other behaviors. If key-value iteration for arrays is to be decided illegal — Ok, I can live with that. But I don't think a couple of lines of code is a big price for the feature that is natural for JS and already exists (and works!) in previous versions. You need a fix anyway, because $index is not handled properly either. Though it could be a minor defect for iteration over objects, it's vital for arrays — now there is no correct index while iterating. |
@diseaz : I spoke with Misko and he said that, while we are supporting iterating over keys, we should also support it over arrays. I will include your change but I suggest the following slight tweak so that we are not unnecessarily updating the DOM if we are just iterating without keys:
If you want to add this and send me a PR then I'll push your commits into my PR. |
Just curious, does this work if the ng-repeat is used with filter?
|
@diseaz : Landed your changes as: petebacondarwin@ee75ccf Thanks for bearing with me. Do post if you find any more issues. |
@petebacondarwin thanks a lot! |
…collection is an array
ok, so I started revewing this PR but after a while I came to realization that it adds too much overhead. the problems it solves are good problems and the solution is quite good too, but given that ngRepeat is one of the most used directives and its already one of the most expensive directives, this change is just too heavy weight for us mainly because the repeater is trying to be too generic and solve all kinds of problems. There is a good news though. What if we solved all the problems in a different, less generic, which also means more lightweight way? Here is how we could do it:
how does this sound to you? |
Here is my syntax proposal.
This is what we would call unstable. Or to put it better, it is stable with respect to the position of the item in the array. So we could rewrite this as:
(The exact syntax is up for discussion) The above means that the DOM elements are associated with the $index and that we use the
The above would be equivalent to the current behavior since the
The above would allow the users to use properties on existing objects such as DB IDs as the DOM association keys. or we could put the hash function on the model
or even on the controller
|
Oh yes, allow the user to specify the key to use for the change detection is a great idea, solves the server side refresh of item list problem well. Thanks for coming to your senses :) How about when iterating over the object fields and using the field name as the key, what would the syntax look like? |
I like @mhevery's suggested syntax, it is powerful and seems fairly intuitive. Would you propose to still support object property iteration? I guess the syntax would be
to keep it backwardly compatible. Under the covers this is really equivalent to:
Making it a stable repeater? The breaking change to people moving from the current sitation to using the unstable repeater (if they don't change their usage), is just that their DOM elements will be rebound to a new scope whenever they reordered their collection rather than being reused and moved around? Would this have a fundamental impact in how it works or just a performance degradation? As an aside, I think that an implementation of this would require a modification to the $watch process in line with #1093. |
While this is a breaking change, I think unless you are animating the movements of the elements in the DOM, it should have very minimal impact on the developer. |
@mhevery @petebacondarwin couldn't you avoid making this a breaking change by defaulting defaulting to a stable iteration using the (newly made public) Can you articulate why it would be a good idea to default to unstable versus defaulting to stable? I think you may underestimate how much code might rely on the stable iteration (at least in my codebases). Wouldn't it be safer to make the unstable iteration opt-in instead of opt-out? Another syntax idea that may be clearer (though perhaps less consistent with normal array comprehension syntax): <div ng-repeat="(key, val) in object using key.id"></div> Just a quick alternative idea. |
@ggoodman can you provide some specific examples where you depend on identity based repeaters? |
@ggoodman I was looking at LINQ query comprehension syntax yesterday which is left-to-right similar to your example. I like that syntax better. http://rthumati.wordpress.com/category/net/02-data-access/linq/ (Scroll down to 2/3 of the page) |
Hi @IgorMinar I think that I may have misunderstood the issue. Can you confirm that the breaking change will only affect arrays/hashs wherein the values are primitives? (Number, String, Boolean)? If that is the case, I do not understand where the breakage is. Now, if the new behaviour would default to using the index/key of items even if the value is an object derivative (and could thus have an $$oid), then I would have a problem. The issue would arise from the fact that I often use If |
I think the names instable and stable are misleading. Instead we should The default would be index-based - which means that each repeated element The other option would be a key-based repeater, where each DOM element is @geoff - I think this is an acceptable change. If the developer upgrades On 5 February 2013 18:33, ggoodman [email protected] wrote:
|
Moreover, @IgorMinar, now that you mentioned Linq, I was thinking that it On 5 February 2013 19:01, Peter Bacon Darwin [email protected] wrote:
|
@petebacondarwin can you elaborate on what the default behaviour would be for enumerating over hashes? Would the new system preserve key => DOM mapping as was the case previously? |
I would assume that repeating over an object's properties would be a On 5 February 2013 20:02, ggoodman [email protected] wrote:
|
as far as I can tell, the issue of the input losing focus hasn't been entirely fixed in angular 1.1.2 http://jsfiddle.net/pUuC5/5/ |
I agree with @mb21. On both 1.0.5 and 1.1.3, I am having this issue still. |
This is definitely a useful option/addition. Is it going to make it in soon? |
temporary solution for 1.0.4, 1.0.3: http://jsfiddle.net/5AAZz/ |
@VELIKII-DIVAN Awesome! Much better work around than the previously suggested directive. This one live updates view bindings rather than waiting for the blur event. Thanks! |
@VELIKII-DIVAN Awesome indeed! |
This is going to be solved a different way so closing this for now. |
I can't find in documentation how I can bind ng-repeat to string. Sometimes it can be useful. List of tags, for example. |
@petebacondarwin Is there an issue that tracks this different way? |
ng-repeat="item in listOfStrings track by $index"? On 6 September 2013 21:47, Marvin Tam [email protected] wrote:
|
Ah ok, didn't realize |
ng-repeat Directive
This complete rewrite of ng-repeat tracks element/scope pairing for objects since they can be tracked by object identity but tracks element/scope pairings for primitive values by the index position in the collection since there is no consistent way of tracking if a primitive has moved.
Fixes primitive issues
It solves various problems including:
Tests
I have added a new test to
ng-repeatSpec.js
to test for this but x-ed out the test that demonstrates the incorrect behaviour.Algorithm
The algorithm checks the following, for
original
collection andchanged
collection, at eachindex
.changed[index] === original[index]
then simply reuse element/scope pairing.changed[index]
andoriginal[index]
are primitives then leave the element alone but update the scope model. This will trigger changes in the this iteration's template bindings and DOM. We can't try to guess if this primitive moved from elsewhere but since we are reusing the element/scope pairing there will be minimal DOM updates.WhatChanged service
I factored out the algorithm that works out what has changed into its own function (
whatChanged
). Since there is potential for this to be reused, I also exposed it as a service,$whatChanged
, in theng
module.Helper Classes
Further to facilitate repeating over object key, the whatChanged algorithm works on a "generic collection", which are created by the internal classes ArrayWrapper and ObjectWrapper. These classes expose a common API of
get(index)
,key(index)
,length()
andcopy()
, which allows the whatChanged algorithm to work either with a straight array or iterating over the keys of an object, cleanly. The array wrapper is very thin and should not be a great impact on performance.These (and a couple of other helper classes
ObjectTracker
,ChangeTracker
andFlattenedChanged
) can be found inapis.js
. These new classes replace the need for HashQueueMap, which could be removed.To Do
There is work to be done on the documentation.
I have done no performance testing. There are more lines of code and probably in most scenarios the repeater may perform worse than before. But I reckon there is lots of room for optimization.