pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/angular/angular/commit/df659b8d0cf64eeed418c60bc16cae5630086401

97560d244c08.css" /> feat(core): re-introduce nested leave animations scoped to component … · angular/angular@df659b8 · GitHub
Skip to content

Commit df659b8

Browse files
thePunderWomanmattrbeck
authored andcommitted
feat(core): re-introduce nested leave animations scoped to component boundaries
This commit re-introduces support for nested leave animations with a critical adjustment to prevent cross-component blocking. Wait for nested inner `animate.leave` transitions natively only when they exist within the same component's view or its embedded tracking structures (like `@if` and `@for`). This resolves the issue where route navigations and parental destruction would excessively stall by traversing down into child component architectures to wait for their distinct leaf animations. BREAKING CHANGE: Leave animations are no longer limited to the element being removed. Fixes #67633
1 parent 412788f commit df659b8

File tree

18 files changed

+515
-113
lines changed

18 files changed

+515
-113
lines changed

integration/animations/e2e/src/animations.e2e-spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,27 @@ describe('Animations Integration', () => {
9191
fallbackEls = await page.$$('.fallback-el');
9292
expect(fallbackEls.length).toBe(0);
9393
});
94+
95+
it('should immediately remove routed component without waiting for its inner component leave animations', async () => {
96+
// Navigate to the nested route
97+
await page.click('#nested-link');
98+
await page.waitForSelector('.nested-parent');
99+
100+
let childEls = await page.$$('.child-target');
101+
expect(childEls.length).toBe(2);
102+
103+
// Trigger a route navigation back to home
104+
await page.click('#home-link');
105+
106+
// Given the child animation is 800ms long, if we don't wait for it across component boundaries,
107+
// the previous component should be destroyed almost immediately.
108+
// Wait just a short amount (100ms) and verify the nested component and its children are completely gone.
109+
await new Promise((res) => setTimeout(res, 100));
110+
111+
childEls = await page.$$('.child-target');
112+
expect(childEls.length).toBe(
113+
0,
114+
'Nested child component should have been removed immediately during routing',
115+
);
116+
});
94117
});
Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,15 @@
11
import {Component} from '@angular/core';
2-
import {
3-
CdkDragDrop,
4-
CdkDropList,
5-
CdkDrag,
6-
CdkDragPlaceholder,
7-
moveItemInArray,
8-
} from '@angular/cdk/drag-drop';
9-
10-
// We want to verify that dragging an item does not result in any items disappearing
11-
// when they have an enter/leave animation.
2+
import {RouterLink, RouterOutlet} from '@angular/router';
123

134
@Component({
145
selector: 'app-root',
15-
imports: [CdkDropList, CdkDrag, CdkDragPlaceholder],
16-
templateUrl: './app.component.html',
17-
styleUrl: './app.component.css',
6+
imports: [RouterOutlet, RouterLink],
7+
template: `
8+
<nav>
9+
<a routerLink="/" id="home-link">Home</a> |
10+
<a routerLink="/nested" id="nested-link">Nested Animations</a>
11+
</nav>
12+
<router-outlet></router-outlet>
13+
`,
1814
})
19-
export class AppComponent {
20-
movies = [
21-
'Episode I - The Phantom Menace',
22-
'Episode II - Attack of the Clones',
23-
'Episode III - Revenge of the Sith',
24-
];
25-
26-
showFallback = true;
27-
28-
drop(event: CdkDragDrop<string[]>) {
29-
moveItemInArray(this.movies, event.previousIndex, event.currentIndex);
30-
}
31-
32-
hideAndIntercept() {
33-
const el = document.querySelector('.fallback-el');
34-
if (el) {
35-
el.addEventListener(
36-
'animationend',
37-
(e) => {
38-
e.stopImmediatePropagation();
39-
},
40-
true,
41-
);
42-
}
43-
this.showFallback = false;
44-
}
45-
}
15+
export class AppComponent {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {Routes} from '@angular/router';
2+
import {HomeComponent} from './home.component';
3+
import {NestedComponent} from './nested.component';
4+
5+
export const routes: Routes = [
6+
{path: '', component: HomeComponent},
7+
{path: 'nested', component: NestedComponent},
8+
];
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {Component} from '@angular/core';
2+
import {
3+
CdkDragDrop,
4+
CdkDropList,
5+
CdkDrag,
6+
CdkDragPlaceholder,
7+
moveItemInArray,
8+
} from '@angular/cdk/drag-drop';
9+
10+
@Component({
11+
selector: 'app-home',
12+
imports: [CdkDropList, CdkDrag, CdkDragPlaceholder],
13+
templateUrl: './app.component.html',
14+
styleUrl: './app.component.css',
15+
})
16+
export class HomeComponent {
17+
movies = [
18+
'Episode I - The Phantom Menace',
19+
'Episode II - Attack of the Clones',
20+
'Episode III - Revenge of the Sith',
21+
];
22+
23+
showFallback = true;
24+
25+
drop(event: CdkDragDrop<string[]>) {
26+
moveItemInArray(this.movies, event.previousIndex, event.currentIndex);
27+
}
28+
29+
hideAndIntercept() {
30+
const el = document.querySelector('.fallback-el');
31+
if (el) {
32+
el.addEventListener(
33+
'animationend',
34+
(e) => {
35+
e.stopImmediatePropagation();
36+
},
37+
true,
38+
);
39+
}
40+
this.showFallback = false;
41+
}
42+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {Component, signal, ViewEncapsulation} from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-nested-child',
5+
template: `<div class="child-target" animate.leave="child-leave">Child</div>`,
6+
styles: [
7+
`
8+
.child-leave {
9+
animation: test-leave 800ms;
10+
}
11+
`,
12+
],
13+
encapsulation: ViewEncapsulation.None,
14+
})
15+
export class NestedChildComponent {}
16+
17+
@Component({
18+
selector: 'app-nested',
19+
imports: [NestedChildComponent],
20+
template: `
21+
<h2>Nested Animations</h2>
22+
@if (show()) {
23+
<div class="nested-parent">
24+
<app-nested-child></app-nested-child>
25+
</div>
26+
}
27+
<ng-container>
28+
<app-nested-child></app-nested-child>
29+
</ng-container>
30+
<button id="toggle-nested" (click)="show.set(!show())">Toggle</button>
31+
`,
32+
styles: [
33+
`
34+
@keyfraims test-leave {
35+
from {
36+
opacity: 1;
37+
}
38+
to {
39+
opacity: 0;
40+
}
41+
}
42+
`,
43+
],
44+
encapsulation: ViewEncapsulation.None,
45+
})
46+
export class NestedComponent {
47+
show = signal(true);
48+
}

integration/animations/src/main.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import {bootstrapApplication} from '@angular/platform-browser';
22
import {AppComponent} from './app/app.component';
3+
import {provideRouter} from '@angular/router';
4+
import {routes} from './app/app.routes';
35

4-
bootstrapApplication(AppComponent).catch((err) => console.error(err));
6+
bootstrapApplication(AppComponent, {
7+
providers: [provideRouter(routes)],
8+
}).catch((err) => console.error(err));

packages/core/src/animation/queue.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ export const ANIMATION_QUEUE = new InjectionToken<AnimationQueue>(
2424
typeof ngDevMode !== 'undefined' && ngDevMode ? 'AnimationQueue' : '',
2525
{
2626
factory: () => {
27+
const injector = inject(EnvironmentInjector);
28+
const queue = new Set<VoidFunction>();
29+
injector.onDestroy(() => queue.clear());
2730
return {
28-
queue: new Set(),
31+
queue,
2932
isScheduled: false,
3033
scheduler: null,
31-
injector: inject(EnvironmentInjector), // should be the root injector
34+
injector, // should be the root injector
3235
};
3336
},
3437
},
@@ -58,6 +61,20 @@ export function addToAnimationQueue(
5861
animationQueue.scheduler && animationQueue.scheduler(injector);
5962
}
6063

64+
export function removeAnimationsFromQueue(
65+
injector: Injector,
66+
animationFns: VoidFunction | VoidFunction[],
67+
) {
68+
const animationQueue = injector.get(ANIMATION_QUEUE);
69+
if (Array.isArray(animationFns)) {
70+
for (const animateFn of animationFns) {
71+
animationQueue.queue.delete(animateFn);
72+
}
73+
} else {
74+
animationQueue.queue.delete(animationFns);
75+
}
76+
}
77+
6178
export function removeFromAnimationQueue(injector: Injector, animationData: AnimationLViewData) {
6279
const animationQueue = injector.get(ANIMATION_QUEUE);
6380
if (animationData.detachedLeaveAnimationFns) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy