fix(ivy): host-styling throws assert exception inside *ngFor (#35133)
Inside `*ngFor` the second run of the styling instructions can get into situation where it tries to read a value from a binding which has not yet executed. As a result the read is `NO_CHANGE` value and subsequent property read cause an exception as it is of wrong type. Fix #35118 PR Close #35133
This commit is contained in:
parent
a8609ba0ad
commit
ab931cf872
|
@ -834,8 +834,20 @@ function findStylingValue(
|
|||
const containsStatics = Array.isArray(rawKey);
|
||||
// Unwrap the key if we contain static values.
|
||||
const key = containsStatics ? (rawKey as string[])[1] : rawKey;
|
||||
let currentValue = key === null ? keyValueArrayGet(lView[index + 1], prop) :
|
||||
key === prop ? lView[index + 1] : undefined;
|
||||
const isStylingMap = key === null;
|
||||
let valueAtLViewIndex = lView[index + 1];
|
||||
if (valueAtLViewIndex === NO_CHANGE) {
|
||||
// In firstUpdatePass the styling instructions create a linked list of styling.
|
||||
// On subsequent passes it is possible for a styling instruction to try to read a binding
|
||||
// which
|
||||
// has not yet executed. In that case we will find `NO_CHANGE` and we should assume that
|
||||
// we have `undefined` (or empty array in case of styling-map instruction) instead. This
|
||||
// allows the resolution to apply the value (which may later be overwritten when the
|
||||
// binding actually executes.)
|
||||
valueAtLViewIndex = isStylingMap ? EMPTY_ARRAY : undefined;
|
||||
}
|
||||
let currentValue = isStylingMap ? keyValueArrayGet(valueAtLViewIndex, prop) :
|
||||
key === prop ? valueAtLViewIndex : undefined;
|
||||
if (containsStatics && !isStylingValuePresent(currentValue)) {
|
||||
currentValue = keyValueArrayGet(rawKey as KeyValueArray<any>, prop);
|
||||
}
|
||||
|
|
|
@ -3273,6 +3273,52 @@ describe('styling', () => {
|
|||
|
||||
expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
describe('regression', () => {
|
||||
onlyInIvy('styling priority resolution is Ivy only feature.')
|
||||
.it('should allow lookahead binding on second pass #35118', () => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: ``,
|
||||
host: {
|
||||
'[class.foo]': 'hostClass',
|
||||
}
|
||||
})
|
||||
class MyCmp {
|
||||
hostClass = true;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[host-styling]',
|
||||
host: {
|
||||
'[class]': 'hostClass',
|
||||
}
|
||||
})
|
||||
class HostStylingsDir {
|
||||
hostClass = {'bar': true};
|
||||
}
|
||||
|
||||
@Component({template: `<my-cmp *ngFor="let i of [1,2]" host-styling></my-cmp>`})
|
||||
class MyApp {
|
||||
// When the first view in the list gets CD-ed, everything works.
|
||||
// When the second view gets CD-ed, the styling has already created the data structures
|
||||
// in the `TView`. As a result when
|
||||
// `[class.foo]` runs it already knows that `[class]` is a duplicate and hence it
|
||||
// should check with it. While the resolution is happening it reads the value of the
|
||||
// `[class]`, however `[class]` has not yet executed and therefore it does not have
|
||||
// normalized value in its `LView`. The result is that the assertions fails as it
|
||||
// expects an
|
||||
// `KeyValueArray`.
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyApp, MyCmp, HostStylingsDir]});
|
||||
const fixture = TestBed.createComponent(MyApp);
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
const [cmp1, cmp2] = fixture.nativeElement.querySelectorAll('my-cmp');
|
||||
expectClass(cmp1).toEqual({foo: true, bar: true});
|
||||
expectClass(cmp2).toEqual({foo: true, bar: true});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function assertStyleCounters(countForSet: number, countForRemove: number) {
|
||||
|
|
|
@ -47,6 +47,9 @@
|
|||
{
|
||||
"name": "EMPTY_ARRAY"
|
||||
},
|
||||
{
|
||||
"name": "EMPTY_ARRAY"
|
||||
},
|
||||
{
|
||||
"name": "EMPTY_OBJ"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue