Skip to content

Commit 98d545f

Browse files
arturovtthePunderWoman
authored andcommitted
fix(animations): cleanup DOM elements when root view is removed with async animations (#53033)
Currently, when using `provideAnimationsAsync`, Angular uses `AnimationRenderer` as the renderer. When the root view is removed, the `AnimationRenderer` defers the actual work to the `TransitionAnimationEngine` to do this, and the `TransitionAnimationEngine` doesn't actually remove the DOM node, but just calls `markElementAsRemoved()`. The actual DOM node is not removed until `TransitionAnimationEngine` "flushes". Unfortunately, though, that "flush" will never happen, since the root view is being destroyed and there will be no more flushes. This commit adds `flush()` call when the root view is being destroyed. PR Close #53033
1 parent 7786e35 commit 98d545f

File tree

3 files changed

+39
-4
lines changed

3 files changed

+39
-4
lines changed

packages/platform-browser/animations/async/src/async_animation_renderer.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
*/
88

99
import {ɵAnimationEngine as AnimationEngine, ɵAnimationRenderer as AnimationRenderer, ɵAnimationRendererFactory as AnimationRendererFactory} from '@angular/animations/browser';
10-
import {inject, NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ɵAnimationRendererType as AnimationRendererType, ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵRuntimeError as RuntimeError} from '@angular/core';
10+
import {inject, Injectable, NgZone, OnDestroy, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, ɵAnimationRendererType as AnimationRendererType, ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵRuntimeError as RuntimeError} from '@angular/core';
1111
import {ɵRuntimeErrorCode as RuntimeErrorCode} from '@angular/platform-browser';
1212

1313
const ANIMATION_PREFIX = '@';
1414

15-
export class AsyncAnimationRendererFactory implements RendererFactory2 {
15+
@Injectable()
16+
export class AsyncAnimationRendererFactory implements OnDestroy, RendererFactory2 {
1617
private _rendererFactoryPromise: Promise<AnimationRendererFactory>|null = null;
1718
private readonly scheduler = inject(ChangeDetectionScheduler, {optional: true});
19+
private _engine?: AnimationEngine;
1820

1921
/**
2022
*
@@ -29,6 +31,17 @@ export class AsyncAnimationRendererFactory implements RendererFactory2 {
2931
ɵAnimationRendererFactory: typeof AnimationRendererFactory
3032
}>) {}
3133

34+
/** @nodoc */
35+
ngOnDestroy(): void {
36+
// When the root view is removed, the renderer defers the actual work to the
37+
// `TransitionAnimationEngine` to do this, and the `TransitionAnimationEngine` doesn't actually
38+
// remove the DOM node, but just calls `markElementAsRemoved()`. The actual DOM node is not
39+
// removed until `TransitionAnimationEngine` "flushes".
40+
// Note: we already flush on destroy within the `InjectableAnimationEngine`. The injectable
41+
// engine is not provided when async animations are used.
42+
this._engine?.flush();
43+
}
44+
3245
/**
3346
* @internal
3447
*/
@@ -47,8 +60,9 @@ export class AsyncAnimationRendererFactory implements RendererFactory2 {
4760
.then(({ɵcreateEngine, ɵAnimationRendererFactory}) => {
4861
// We can't create the renderer yet because we might need the hostElement and the type
4962
// Both are provided in createRenderer().
50-
const engine = ɵcreateEngine(this.animationType, this.doc, this.scheduler);
51-
const rendererFactory = new ɵAnimationRendererFactory(this.delegate, engine, this.zone);
63+
this._engine = ɵcreateEngine(this.animationType, this.doc, this.scheduler);
64+
const rendererFactory =
65+
new ɵAnimationRendererFactory(this.delegate, this._engine, this.zone);
5266
this.delegate = rendererFactory;
5367
return rendererFactory;
5468
});

packages/platform-browser/animations/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ts_library(
2323
"//packages/platform-browser",
2424
"//packages/platform-browser-dynamic",
2525
"//packages/platform-browser/animations",
26+
"//packages/platform-browser/animations/async",
2627
"//packages/platform-browser/testing",
2728
"//packages/private/testing",
2829
"@npm//rxjs",

packages/platform-browser/animations/test/animation_renderer_spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {TestBed} from '@angular/core/testing';
1212
import {bootstrapApplication} from '@angular/platform-browser';
1313
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
1414
import {BrowserAnimationsModule, ɵInjectableAnimationEngine as InjectableAnimationEngine} from '@angular/platform-browser/animations';
15+
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
1516
import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer';
1617
import {withBody} from '@angular/private/testing';
1718

@@ -447,6 +448,25 @@ describe('destroy', () => {
447448

448449
appRef.destroy();
449450

451+
expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed
452+
expect(document.body.childNodes.length).toEqual(2); // other elements are
453+
}));
454+
455+
it('should clear bootstrapped component contents when async animations are used',
456+
withBody('<div>before</div><app-root></app-root><div>after</div>', async () => {
457+
@Component({selector: 'app-root', template: 'app-root content', standalone: true})
458+
class AppComponent {
459+
}
460+
461+
const appRef =
462+
await bootstrapApplication(AppComponent, {providers: [provideAnimationsAsync()]});
463+
464+
const root = document.body.querySelector('app-root')!;
465+
expect(root.textContent).toEqual('app-root content');
466+
expect(document.body.childNodes.length).toEqual(3);
467+
468+
appRef.destroy();
469+
450470
expect(document.body.querySelector('app-root')).toBeFalsy(); // host element is removed
451471
expect(document.body.childNodes.length).toEqual(2); // other elements are
452472
}));

0 commit comments

Comments
 (0)