Skip to content

Commit 70a5b65

Browse files
thePunderWomanleonsenft
authored andcommitted
fix(core): prevent element duplication with dynamic components
When dynamic components are rapidly added and removed with animate.leave and animate.enter, a leave animation might fire before the enter animation could, causing an element to be retained. This fix prevents that from occuring by clearing the enter animations in this case. fixes: #66794 (cherry picked from commit c66a19f)
1 parent 4c7126d commit 70a5b65

File tree

11 files changed

+128
-1
lines changed

11 files changed

+128
-1
lines changed

packages/core/src/animation/queue.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,17 @@ export function queueEnterAnimations(
100100
addToAnimationQueue(injector, nodeAnimations.animateFns);
101101
}
102102
}
103+
104+
export function removeAnimationsFromQueue(
105+
injector: Injector,
106+
animationFns: VoidFunction | VoidFunction[],
107+
) {
108+
const animationQueue = injector.get(ANIMATION_QUEUE);
109+
if (Array.isArray(animationFns)) {
110+
for (const animateFn of animationFns) {
111+
animationQueue.queue.delete(animateFn);
112+
}
113+
} else {
114+
animationQueue.queue.delete(animationFns);
115+
}
116+
}

packages/core/src/render3/node_manipulation.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ import {ProfilerEvent} from '../../primitives/devtools';
8484
import {getLViewParent, getNativeByTNode, unwrapRNode} from './util/view_utils';
8585
import {allLeavingAnimations} from '../animation/longest_animation';
8686
import {Injector} from '../di';
87-
import {addToAnimationQueue, queueEnterAnimations} from '../animation/queue';
87+
import {
88+
addToAnimationQueue,
89+
queueEnterAnimations,
90+
removeAnimationsFromQueue,
91+
} from '../animation/queue';
8892
import {RunLeaveAnimationFn} from '../animation/interfaces';
8993

9094
const enum WalkTNodeTreeAction {
@@ -385,6 +389,10 @@ function runLeaveAnimationsWithCallback(
385389
callback: Function,
386390
) {
387391
const animations = lView?.[ANIMATIONS];
392+
if (animations?.enter?.has(tNode.index)) {
393+
removeAnimationsFromQueue(injector, animations.enter.get(tNode.index)!.animateFns);
394+
}
395+
388396
if (animations == null || animations.leave == undefined || !animations.leave.has(tNode.index))
389397
return callback(false);
390398

packages/core/test/acceptance/animation_spec.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
OnDestroy,
2323
provideZonelessChangeDetection,
2424
signal,
25+
TemplateRef,
2526
ViewChild,
2627
ViewContainerRef,
2728
} from '@angular/core';
@@ -2299,4 +2300,100 @@ describe('Animation', () => {
22992300
expect(fixture.debugElement.query(By.css('p.not-here'))).toBeNull();
23002301
}));
23012302
});
2303+
2304+
describe('animation element duplication', () => {
2305+
it('should not duplicate elements when using dynamic components', async () => {
2306+
const animateStyles = `
2307+
.example-menu {
2308+
display: inline-flex;
2309+
flex-direction: column;
2310+
min-width: 180px;
2311+
max-width: 280px;
2312+
padding: 6px 0;
2313+
}
2314+
.open {
2315+
animation: open 10ms;
2316+
}
2317+
.close {
2318+
animation: open 10ms reverse;
2319+
}
2320+
@keyframes open {
2321+
from {
2322+
opacity: 0;
2323+
}
2324+
to {
2325+
opacity: 1;
2326+
}
2327+
}
2328+
`;
2329+
2330+
@Component({
2331+
selector: 'dynamic-menu',
2332+
styles: [animateStyles],
2333+
template: `
2334+
<ng-template #menu>
2335+
<div class="example-menu" animate.enter="open" animate.leave="close">
2336+
<div>Menu</div>
2337+
</div>
2338+
</ng-template>
2339+
`,
2340+
changeDetection: ChangeDetectionStrategy.OnPush,
2341+
encapsulation: ViewEncapsulation.None,
2342+
})
2343+
class MenuComponent {
2344+
@ViewChild('menu') menuTpl!: TemplateRef<unknown>;
2345+
vcr = inject(ViewContainerRef);
2346+
opened = false;
2347+
2348+
toggle() {
2349+
if (this.opened) {
2350+
this.close();
2351+
} else {
2352+
this.open();
2353+
}
2354+
}
2355+
2356+
open() {
2357+
this.opened = true;
2358+
this.vcr.createEmbeddedView(this.menuTpl);
2359+
}
2360+
2361+
close() {
2362+
this.opened = false;
2363+
this.vcr.clear();
2364+
}
2365+
}
2366+
2367+
@Component({
2368+
selector: 'test-cmp',
2369+
imports: [MenuComponent],
2370+
template: ` <dynamic-menu /> `,
2371+
encapsulation: ViewEncapsulation.None,
2372+
})
2373+
class TestComponent {}
2374+
2375+
TestBed.configureTestingModule({animationsEnabled: true});
2376+
const fixture = TestBed.createComponent(TestComponent);
2377+
fixture.detectChanges();
2378+
2379+
const cmp = fixture.debugElement.query(By.css('dynamic-menu')).componentInstance;
2380+
2381+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
2382+
2383+
// Toggle the menu quickly multiple times
2384+
for (let i = 0; i < 20; i++) {
2385+
cmp.toggle();
2386+
// Wait 1ms to allow some logic to run but less than animation duration
2387+
await delay(1);
2388+
fixture.detectChanges();
2389+
}
2390+
2391+
// Finish remaining animations (wait 100ms real time which is > 10ms animation)
2392+
await delay(200);
2393+
fixture.detectChanges();
2394+
2395+
const menus = fixture.debugElement.nativeElement.querySelectorAll('.example-menu');
2396+
expect(menus.length).toBe(0);
2397+
});
2398+
});
23022399
});

packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,7 @@
756756
"relativePath",
757757
"rememberChangeHistoryAndInvokeOnChangesHook",
758758
"remove",
759+
"removeAnimationsFromQueue",
759760
"removeClass",
760761
"removeElements",
761762
"removeFromArray",

packages/core/test/bundling/create_component/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@
604604
"relativePath",
605605
"rememberChangeHistoryAndInvokeOnChangesHook",
606606
"remove",
607+
"removeAnimationsFromQueue",
607608
"removeElements",
608609
"removeFromArray",
609610
"removeLViewOnDestroy",

packages/core/test/bundling/defer/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@
656656
"registerPreOrderHooks",
657657
"rememberChangeHistoryAndInvokeOnChangesHook",
658658
"remove",
659+
"removeAnimationsFromQueue",
659660
"removeFromArray",
660661
"removeLViewFromLContainer",
661662
"removeLViewOnDestroy",

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,7 @@
875875
"relativePath",
876876
"rememberChangeHistoryAndInvokeOnChangesHook",
877877
"remove",
878+
"removeAnimationsFromQueue",
878879
"removeElements",
879880
"removeFromArray",
880881
"removeLViewOnDestroy",

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,7 @@
873873
"relativePath",
874874
"rememberChangeHistoryAndInvokeOnChangesHook",
875875
"remove",
876+
"removeAnimationsFromQueue",
876877
"removeElements",
877878
"removeFromArray",
878879
"removeLViewOnDestroy",

packages/core/test/bundling/hydration/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,7 @@
697697
"relativePath",
698698
"rememberChangeHistoryAndInvokeOnChangesHook",
699699
"remove",
700+
"removeAnimationsFromQueue",
700701
"removeDehydratedView",
701702
"removeDehydratedViews",
702703
"removeElements",

packages/core/test/bundling/router/bundle.golden_symbols.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,7 @@
994994
"relativePath",
995995
"rememberChangeHistoryAndInvokeOnChangesHook",
996996
"remove",
997+
"removeAnimationsFromQueue",
997998
"removeElements",
998999
"removeFromArray",
9991000
"removeLViewOnDestroy",

0 commit comments

Comments
 (0)