fix(animations): do not leak DOM nodes/styling for host triggered animations (#18853)
Closes #18606 PR Close #18853
This commit is contained in:
parent
fd701b07f0
commit
fcadeb2079
|
@ -837,7 +837,7 @@ export class TransitionAnimationEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
const allLeaveNodes: any[] = [];
|
const allLeaveNodes: any[] = [];
|
||||||
const leaveNodesWithoutAnimations: any[] = [];
|
const leaveNodesWithoutAnimations = new Set<any>();
|
||||||
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
|
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
|
||||||
const element = this.collectedLeaveElements[i];
|
const element = this.collectedLeaveElements[i];
|
||||||
const details = element[REMOVAL_FLAG] as ElementAnimationState;
|
const details = element[REMOVAL_FLAG] as ElementAnimationState;
|
||||||
|
@ -845,7 +845,7 @@ export class TransitionAnimationEngine {
|
||||||
addClass(element, LEAVE_CLASSNAME);
|
addClass(element, LEAVE_CLASSNAME);
|
||||||
allLeaveNodes.push(element);
|
allLeaveNodes.push(element);
|
||||||
if (!details.hasAnimation) {
|
if (!details.hasAnimation) {
|
||||||
leaveNodesWithoutAnimations.push(element);
|
leaveNodesWithoutAnimations.add(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -937,12 +937,14 @@ export class TransitionAnimationEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// these can only be detected here since we have a map of all the elements
|
// these can only be detected here since we have a map of all the elements
|
||||||
// that have animations attached to them...
|
// that have animations attached to them... We use a set here in the event
|
||||||
const enterNodesWithoutAnimations: any[] = [];
|
// multiple enter captures on the same element were caught in different
|
||||||
|
// renderer namespaces (e.g. when a @trigger was on a host binding that had *ngIf)
|
||||||
|
const enterNodesWithoutAnimations = new Set<any>();
|
||||||
for (let i = 0; i < allEnterNodes.length; i++) {
|
for (let i = 0; i < allEnterNodes.length; i++) {
|
||||||
const element = allEnterNodes[i];
|
const element = allEnterNodes[i];
|
||||||
if (!subTimelines.has(element)) {
|
if (!subTimelines.has(element)) {
|
||||||
enterNodesWithoutAnimations.push(element);
|
enterNodesWithoutAnimations.add(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1435,9 +1437,11 @@ function cloakElement(element: any, value?: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloakAndComputeStyles(
|
function cloakAndComputeStyles(
|
||||||
driver: AnimationDriver, elements: any[], elementPropsMap: Map<any, Set<string>>,
|
driver: AnimationDriver, elements: Set<any>, elementPropsMap: Map<any, Set<string>>,
|
||||||
defaultStyle: string): [Map<any, ɵStyleData>, any[]] {
|
defaultStyle: string): [Map<any, ɵStyleData>, any[]] {
|
||||||
const cloakVals = elements.map(element => cloakElement(element));
|
const cloakVals: string[] = [];
|
||||||
|
elements.forEach(element => cloakVals.push(cloakElement(element)));
|
||||||
|
|
||||||
const valuesMap = new Map<any, ɵStyleData>();
|
const valuesMap = new Map<any, ɵStyleData>();
|
||||||
const failedElements: any[] = [];
|
const failedElements: any[] = [];
|
||||||
|
|
||||||
|
@ -1456,7 +1460,10 @@ function cloakAndComputeStyles(
|
||||||
valuesMap.set(element, styles);
|
valuesMap.set(element, styles);
|
||||||
});
|
});
|
||||||
|
|
||||||
elements.forEach((element, i) => cloakElement(element, cloakVals[i]));
|
// we use a index variable here since Set.forEach(a, i) does not return
|
||||||
|
// an index value for the closure (but instead just the value)
|
||||||
|
let i = 0;
|
||||||
|
elements.forEach(element => cloakElement(element, cloakVals[i++]));
|
||||||
return [valuesMap, failedElements];
|
return [valuesMap, failedElements];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -733,6 +733,46 @@ export function main() {
|
||||||
flushMicrotasks();
|
flushMicrotasks();
|
||||||
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
|
expect(fixture.debugElement.nativeElement.children.length).toBe(0);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should properly evaluate pre/auto-style values when components are inserted/removed which contain host animations',
|
||||||
|
fakeAsync(() => {
|
||||||
|
@Component({
|
||||||
|
selector: 'parent-cmp',
|
||||||
|
template: `
|
||||||
|
<child-cmp *ngFor="let item of items"></child-cmp>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class ParentCmp {
|
||||||
|
items: any[] = [1, 2, 3, 4, 5];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'child-cmp',
|
||||||
|
template: '... child ...',
|
||||||
|
animations:
|
||||||
|
[trigger('host', [transition(':leave', [animate(1000, style({opacity: 0}))])])]
|
||||||
|
})
|
||||||
|
class ChildCmp {
|
||||||
|
@HostBinding('@host') public hostAnimation = 'a';
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]});
|
||||||
|
|
||||||
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
|
const fixture = TestBed.createComponent(ParentCmp);
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
const element = fixture.nativeElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
cmp.items = [0, 2, 4, 6]; // 1,3,5 get removed
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const items = element.querySelectorAll('child-cmp');
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
expect(item.style['display']).toBeFalsy();
|
||||||
|
}
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should cancel and merge in mid-animation styles into the follow-up animation, but only for animation keyframes that start right away',
|
it('should cancel and merge in mid-animation styles into the follow-up animation, but only for animation keyframes that start right away',
|
||||||
|
|
Loading…
Reference in New Issue