ngClass[Odd/Even] overhaul#15228
Conversation
The code was added in b41fe9f in order to support having both `ngClass` and interpolation in `class` work together. `ngClass` has changed considerably since b41fe9f and for simple cases to work the `$observe`r is no longer necessary (as indicated by the expanded test still passing). That said, it is a [documented known issue][1] that `ngClass` should not be used together with interpolation in `class` and more complicated cases do not work anyway. [1]: https://docs.angularjs.org/api/ng/directive/ngClass#known-issues
…ide arrays Mostly taken from angular#14404.
| } | ||
|
|
||
| attr.$addClass(toAdd); | ||
| attr.$removeClass(toRemove); |
There was a problem hiding this comment.
I never knew about these helpers!!
| 'use strict'; | ||
|
|
||
| describe('ngClass', function() { | ||
| fdescribe('ngClass', function() { |
There was a problem hiding this comment.
I already have a fixup commit at the end 😛
| expect(e2).not.toHaveClass('four'); | ||
| expect(e2).toHaveClass('five'); | ||
|
|
||
| $rootScope.$apply('two = "too"'); |
There was a problem hiding this comment.
although this is very clever "two" and "too" are very similar and actually make the test harder to follow.
There was a problem hiding this comment.
The cleverness credits go to the original test 😃
I can change it.
| }) | ||
| ); | ||
|
|
||
| it('should do value remove the watcher when static map one-time binding', |
There was a problem hiding this comment.
the test description here seems like a hybrid of the two tests above :-)
| inject(function($rootScope, $compile) { | ||
| $rootScope.classVar = [{orange: true}]; | ||
| element = $compile('<div ng-class="classVar"></div>')($rootScope); | ||
| $rootScope.$digest(); |
There was a problem hiding this comment.
Is it worth adding an expectation that it should have the class "orange" here?
| })); | ||
|
|
||
|
|
||
| it('should keep track of old classes even if odd/even does not match `$index` atm', |
There was a problem hiding this comment.
This description makes hard to understand what is actually being tested.
Are you saying that ngClass is not removing old classes when both the $index and the expression value changes?
There was a problem hiding this comment.
It might be because I was trying to solve it differently at first. Here is what was happening before this change:
- Assume
ng-class-odd="foo",$index = 0,foo = 'foo'. The element hasclassName === 'foo'. - If the
$indexandfoochange simultaneously, say$index = 1,foo = 'bar', then:- The
attr[name]watch kicks in first and it checks ifscope.$index & 1 === selector(i.e. it uses the current value of$indexto determine if it needs to update the classes). It decides it doesn't need to do anything, so at this point the element still hasclassName === 'foo'. - The
$indexwatch kicks in. Since it determines that$index & 1 !== old$index & 1, it decides to remove the classes. But it tries toscope.$eval(attr[name])which now evaluates to'bar'. So the element will still haveclassName === 'foo'.
- The
Basically, the problem is that both watch-actions check against the current value of each other to determine what to do, which fails when both change in the same digest.
I.e. the attr[name] watch uses scope.$index (while it should use the previous index) and the $index watch uses scope.$eval(attr[name]) (while it should use the previous value - the "old classes").
The commit fxes it by:
- Moving the
$indexwatch before theattr[name]watch. - Using
oldValfrom the$indexwatch to add/remove.
This way, when the $index & 1 changes, the $index watch will correctly add/remove the classes and then the attr[name] watch will update the classes (i.e. add the new ones) if necessary.
I hope this makes sense 😃
How about:
It should add/remove the correct classes when the expression and
$indexchange simultaneously
petebacondarwin
left a comment
There was a problem hiding this comment.
When using large objects as keys (e.g.:
{loaded: $ctrl.data}).
I think you mean "When using large objects as values...", right?
| 'use strict'; | ||
|
|
||
| fdescribe('ngClass', function() { | ||
| describe('ngClass', function() { |
|
A few minor comments but generally looks good to me. |
The cases that should benefit most are:
1. When using large objects as values (e.g.: `{loaded: $ctrl.data}`).
2. When using objects/arrays and there are frequent changes.
3. When there are many `$index` changes (e.g. addition/deletion/reordering in large `ngRepeat`s).
The differences in operations per digest include:
1. `Regular expression (when not changed)`
**Before:** `equals()`
**After:** `toClassString()`
2. `Regular expression (when changed)`
**Before:** `copy()` + 2 x `arrayClasses()` + `shallowCopy()`
**After:** 2 x `split()`
3. `One-time expression (when not changed)`
**Before:** `equals()`
**After:** `toFlatValue()` + `equals()`*
4. `One-time expression (when changed)`
**Before:** `copy()` + 2 x `arrayClasses()` + `shallowCopy()`
**After:** `copy()`* + `toClassString()`* + 2 x `split()`
5. `$index modulo changed`
**Before:** `arrayClasses()`
**After:** -
(*): on flatter structure
In large based on angular#14404. Kudos to @drpicox for the initial idea and a big part
of the implementation.
Closes angular#14404
…cts inside arrays
…do not match index Change commit message to: fix(ngClassOdd/Even): add/remove the correct classes when expression/`$index` change simultaneously
516fdb3 to
dfe4307
Compare
|
Thx for the review @petebacondarwin, @jbedard. I have added some fixup/squash commits addressing the comments (except for the parse interceptor for PTAL |
|
LGTM |
| function arrayDifference(tokens1, tokens2) { | ||
| var values = []; | ||
| // Helpers | ||
| function arrayDifference(tokens1, tokens2) { |
There was a problem hiding this comment.
Maybe this will be better ?
function arrayDifference(tokens1, tokens2) {
var tokens1Length = tokens1.length, tokens2Length = tokens2.length;
if (!tokens1 || !tokens1Length) return [];
if (!tokens2 || !tokens2Length) return tokens1;
var values = [];
outer:
for (var i = 0; i < tokens1Length; i++) {
var token = tokens1[i];
for (var j = 0; j < tokens2Length; j++) {
if (token === tokens2[j]) continue outer;
}
values.push(token);
}
return values;
}
There was a problem hiding this comment.
I thought about that too. The "problem" is that tokens1/tokens2 can be undefined. So, "caching" their length gets more complicated (to the point it shouldn't make any difference).
I also thought about just caching it for the for loop (e.g. for (var i = 0, ii = tokens1.length; i < ii; i++)), but I think the VM will detect that there is nothing mutating .length inside the loop and make that optimisation itself (this is just an assumption - maybe I am wrong).
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Fix, Test, Refactor, Perf
What is the current behavior? (You can also link to an open issue here)
Broken, inefficient.
What is the new behavior (if this is a feature change)?
Fixed, efficient.
Does this PR introduce a breaking change?
No (to the best of my knowledge).
Please check if the PR fulfills these requirements
Docs have been added / updated (for bug fixes / features)Other information:
Supercedes (and partly based on) #14404. Mad props to @drpicox for the initial idea and implementation.
This PR has been broken up in smaller, focused commits for easier reviewing. Better to review commit by commit (and probably in order). Most commits are (more or less) trivial - except for the last one.
Refactoring commits:
$animate$observerarrayDifference()early if an input is emptyTest commits:
Fix commits:
Perf commits:
dataonceCloses #14404