-
Notifications
You must be signed in to change notification settings - Fork 27.4k
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never knew about these helpers!!
@@ -1,6 +1,6 @@ | |||
'use strict'; | |||
|
|||
describe('ngClass', function() { | |||
fdescribe('ngClass', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naught naughty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth adding an expectation that it should have the class "orange" here?
@@ -462,6 +462,47 @@ fdescribe('ngClass', function() { | |||
expect(e2.hasClass('odd')).toBeFalsy(); | |||
})); | |||
|
|||
|
|||
it('should keep track of old classes even if odd/even does not match `$index` atm', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
$index
andfoo
change 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$index
to 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
$index
watch 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
$index
watch before theattr[name]
watch. - Using
oldVal
from the$index
watch 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
$index
change simultaneously
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When using large objects as keys (e.g.:
{loaded: $ctrl.data}
).
I think you mean "When using large objects as values...", right?
@@ -1,6 +1,6 @@ | |||
'use strict'; | |||
|
|||
fdescribe('ngClass', function() { | |||
describe('ngClass', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahah!
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
$observe
rarrayDifference()
early if an input is emptyTest commits:
Fix commits:
Perf commits:
data
onceCloses #14404