Content-Length: 693884 | pFad | https://github.com/angular/angular/commit/0b69b619296231edfab0561480296c477e2c72ca

0A fix(core): Flush animations when no component has been checked (#58089) · angular/angular@0b69b61 · GitHub
Skip to content

Commit 0b69b61

Browse files
JeanMecheatscott
authored andcommitted
fix(core): Flush animations when no component has been checked (#58089)
When Angular runs application synchronization automatically, animations are now guaranteed to be flushed, regardless of whether change detection was run on any components attached to `ApplicationRef`. This most frequently affects animations related to component removal where the DOM element for the component would previously not be removed due to animations not being flushed BREAKING CHANGE: Animations are guaranteed to be flushed when Angular runs automatic change detection or manual calls to `ApplicationRef.tick`. Prior to this change, animations would not be flushed in some situations if change detection did not run on any views attached to the application. This change can affect tests which may rely on the old behavior, often by making assertions on DOM elements that should have been removed but weren't because DOM removal is delayed until animations are flushed. fixes #58075 PR Close #58089
1 parent bf0f4c4 commit 0b69b61

File tree

5 files changed

+96
-27
lines changed

5 files changed

+96
-27
lines changed

packages/core/src/application/application_ref.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,7 @@ export class ApplicationRef {
666666
}
667667

668668
// First check dirty views, if there are any.
669+
let ranDetectChanges = false;
669670
if (this.dirtyFlags & ApplicationRefDirtyFlags.ViewTreeAny) {
670671
// Change detection on views starts in targeted mode (only check components if they're
671672
// marked as dirty) unless global checking is specifically requested via APIs like
@@ -680,7 +681,21 @@ export class ApplicationRef {
680681

681682
// Check all potentially dirty views.
682683
for (let {_lView} of this.allViews) {
683-
detectChangesInViewIfRequired(_lView, useGlobalCheck, this.zonelessEnabled);
684+
// When re-checking, only check views which actually need it.
685+
if (!useGlobalCheck && !requiresRefreshOrTraversal(_lView)) {
686+
continue;
687+
}
688+
689+
const mode =
690+
useGlobalCheck && !this.zonelessEnabled
691+
? // Global mode includes `CheckAlways` views.
692+
// When using zoneless, all root views must be explicitly marked for refresh, even if they are
693+
// `CheckAlways`.
694+
ChangeDetectionMode.Global
695+
: // Only refresh views with the `RefreshView` flag or views is a changed signal
696+
ChangeDetectionMode.Targeted;
697+
detectChangesInternal(_lView, mode);
698+
ranDetectChanges = true;
684699
}
685700

686701
// If `markForCheck()` was called during view checking, it will have set the `ViewTreeCheck`
@@ -698,7 +713,8 @@ export class ApplicationRef {
698713
// hooks.
699714
return;
700715
}
701-
} else {
716+
}
717+
if (!ranDetectChanges) {
702718
// If we skipped refreshing views above, there might still be unflushed animations
703719
// because we never called `detectChangesInternal` on the views.
704720
this._rendererFactory?.begin?.();
@@ -899,24 +915,3 @@ export const enum ApplicationRefDirtyFlags {
899915
*/
900916
RootEffects = 0b00010000,
901917
}
902-
903-
export function detectChangesInViewIfRequired(
904-
lView: LView,
905-
isFirstPass: boolean,
906-
zonelessEnabled: boolean,
907-
) {
908-
// When re-checking, only check views which actually need it.
909-
if (!isFirstPass && !requiresRefreshOrTraversal(lView)) {
910-
return;
911-
}
912-
913-
const mode =
914-
isFirstPass && !zonelessEnabled
915-
? // The first pass is always in Global mode, which includes `CheckAlways` views.
916-
// When using zoneless, all root views must be explicitly marked for refresh, even if they are
917-
// `CheckAlways`.
918-
ChangeDetectionMode.Global
919-
: // Only refresh views with the `RefreshView` flag or views is a changed signal
920-
ChangeDetectionMode.Targeted;
921-
detectChangesInternal(lView, mode);
922-
}

packages/core/src/core_private_export.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export {
2222
type NavigationDestination as ɵNavigationDestination,
2323
} from '../primitives/dom-navigation';
2424
export {setAlternateWeakRefImpl as ɵsetAlternateWeakRefImpl} from '../primitives/signals';
25-
export {detectChangesInViewIfRequired as ɵdetectChangesInViewIfRequired} from './application/application_ref';
2625
export {INTERNAL_APPLICATION_ERROR_HANDLER as ɵINTERNAL_APPLICATION_ERROR_HANDLER} from './error_handler';
2726
export {
2827
IMAGE_CONFIG as ɵIMAGE_CONFIG,

packages/core/test/acceptance/renderer_factory_spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ describe('renderer factory lifecycle', () => {
114114
'create',
115115
'create',
116116
'begin',
117+
'end',
118+
'begin',
117119
'some_component create',
118120
'some_component update',
119121
'end',
@@ -128,7 +130,7 @@ describe('renderer factory lifecycle', () => {
128130
const fixture = TestBed.createComponent(SomeComponentWhichThrows);
129131
fixture.componentRef.changeDetectorRef.detectChanges();
130132
}).toThrow();
131-
expect(logs).toEqual(['create', 'create', 'begin', 'end']);
133+
expect(logs).toEqual(['create', 'create', 'begin', 'end', 'begin', 'end']);
132134
});
133135

134136
it('should pass in the component styles directly into the underlying renderer', () => {

packages/core/test/change_detection_scheduler_spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ApplicationRef,
1515
ChangeDetectorRef,
1616
Component,
17+
ComponentRef,
1718
createComponent,
1819
destroyPlatform,
1920
ElementRef,
@@ -51,6 +52,7 @@ import {RuntimeError, RuntimeErrorCode} from '../src/errors';
5152
import {scheduleCallbackWithRafRace} from '../src/util/callback_scheduler';
5253
import {ChangeDetectionSchedulerImpl} from '../src/change_detection/scheduling/zoneless_scheduling_impl';
5354
import {global} from '../src/util/global';
55+
import {provideNoopAnimations} from '@angular/platform-browser/animations';
5456

5557
function isStable(injector = TestBed.inject(EnvironmentInjector)): boolean {
5658
return toSignal(injector.get(ApplicationRef).isStable, {requireSync: true, injector})();
@@ -375,6 +377,77 @@ describe('Angular with zoneless enabled', () => {
375377
expect(isStable()).toBe(false);
376378
});
377379

380+
it(
381+
'when attaching view to ApplicationRef with animations',
382+
withBody('<app></app>', async () => {
383+
destroyPlatform();
384+
385+
@Component({
386+
standalone: true,
387+
template: `<p>Component created</p>`,
388+
})
389+
class DynamicComponent {
390+
cdr = inject(ChangeDetectorRef);
391+
}
392+
393+
@Component({
394+
selector: 'app',
395+
standalone: true,
396+
template: `<main #outlet></main>`,
397+
})
398+
class App {
399+
@ViewChild('outlet') outlet!: ElementRef<HTMLElement>;
400+
401+
envInjector = inject(EnvironmentInjector);
402+
appRef = inject(ApplicationRef);
403+
elementRef = inject(ElementRef);
404+
405+
createComponent() {
406+
const host = document.createElement('div');
407+
this.outlet.nativeElement.appendChild(host);
408+
409+
const ref = createComponent(DynamicComponent, {
410+
environmentInjector: this.envInjector,
411+
hostElement: host,
412+
});
413+
414+
this.appRef.attachView(ref.hostView);
415+
416+
return ref;
417+
}
418+
}
419+
420+
const applicationRef = await bootstrapApplication(App, {
421+
providers: [
422+
provideZonelessChangeDetection(),
423+
provideNoopAnimations(),
424+
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
425+
],
426+
});
427+
428+
const component = applicationRef.components[0] as ComponentRef<App>;
429+
const appNativeElement = component.instance.elementRef.nativeElement;
430+
431+
await applicationRef.whenStable();
432+
expect(appNativeElement.innerHTML).toEqual('<main></main>');
433+
434+
const ref: ComponentRef<DynamicComponent> = component.instance.createComponent();
435+
await applicationRef.whenStable();
436+
expect(appNativeElement.innerHTML).toContain('<p>Component created</p>');
437+
438+
// Similating a case where invoking destroy also schedules a CD.
439+
ref.instance.cdr.markForCheck();
440+
ref.destroy();
441+
442+
// DOM is not synchronously removed because change detection hasn't run
443+
expect(appNativeElement.innerHTML).toContain('<p>Component created</p>');
444+
await applicationRef.whenStable();
445+
446+
expect(isStable()).toBe(true);
447+
expect(appNativeElement.innerHTML).toEqual('<main></main>');
448+
}),
449+
);
450+
378451
it('when a stable subscription synchronously causes another notification', async () => {
379452
const val = signal('initial');
380453
@Component({template: '{{val()}}'})

packages/core/test/render3/change_detection_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ describe('change detection', () => {
3232
rendererFactory.end = () => log.push('end');
3333

3434
const fixture = TestBed.createComponent(MyComponent);
35-
fixture.detectChanges();
35+
log.length = 0;
36+
fixture.changeDetectorRef.detectChanges();
3637

3738
expect(fixture.nativeElement.innerHTML).toEqual('works');
3839

3940
expect(log).toEqual([
4041
'begin',
4142
'detect changes', // regular change detection cycle
4243
'end',
43-
'detect changes', // check no changes cycle
4444
]);
4545
}),
4646
);

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


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

Fetched URL: https://github.com/angular/angular/commit/0b69b619296231edfab0561480296c477e2c72ca

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy