mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(core): Add scheduler abstraction and notify when necessary (#53499)
In order to provide a reasonable experience for Angular without Zones, we need a mechanism to run change detection when we receive a change notification. There are several existing APIs today that serve as the change notification: `ChangeDetectorRef.markForCheck`, signal updates, event listeners (since they mark the view dirty), and attaching a view to either the `ApplicationRef` or `ChangeDetectorRef`. These operations are now paired with a notification to the change detection scheduler. The concrete implementation for this scheduler is still being designed. However, this gives us a starting point to partner with teams to experiment with what that might look like. PR Close #53499
This commit is contained in:
committed by
Andrew Scott
parent
cc74ebfdf6
commit
8d58595478
@@ -8,7 +8,7 @@
|
||||
|
||||
import {Subscription} from 'rxjs';
|
||||
|
||||
import {provideZoneChangeDetection} from '../change_detection/scheduling';
|
||||
import {provideZoneChangeDetection} from '../change_detection/scheduling/ng_zone_scheduling';
|
||||
import {EnvironmentProviders, Provider, StaticProvider} from '../di/interface/provider';
|
||||
import {EnvironmentInjector} from '../di/r3_injector';
|
||||
import {ErrorHandler} from '../error_handler';
|
||||
|
||||
+7
-7
@@ -8,13 +8,13 @@
|
||||
|
||||
import {Subscription} from 'rxjs';
|
||||
|
||||
import {ApplicationRef} from '../application/application_ref';
|
||||
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, Injectable, InjectionToken, makeEnvironmentProviders, StaticProvider} from '../di';
|
||||
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../error_handler';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
import {PendingTasks} from '../pending_tasks';
|
||||
import {NgZone} from '../zone';
|
||||
import {InternalNgZoneOptions} from '../zone/ng_zone';
|
||||
import {ApplicationRef} from '../../application/application_ref';
|
||||
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, Injectable, InjectionToken, makeEnvironmentProviders, StaticProvider} from '../../di';
|
||||
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../../error_handler';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../../errors';
|
||||
import {PendingTasks} from '../../pending_tasks';
|
||||
import {NgZone} from '../../zone';
|
||||
import {InternalNgZoneOptions} from '../../zone/ng_zone';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class NgZoneChangeDetectionScheduler {
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* Injectable that is notified when an `LView` is made aware of changes to application state.
|
||||
*/
|
||||
export abstract class ChangeDetectionScheduler {
|
||||
abstract notify(): void;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export * from './di';
|
||||
export {BootstrapOptions, ApplicationRef, NgProbeToken, APP_BOOTSTRAP_LISTENER} from './application/application_ref';
|
||||
export {PlatformRef} from './platform/platform_ref';
|
||||
export {createPlatform, createPlatformFactory, assertPlatform, destroyPlatform, getPlatform} from './platform/platform';
|
||||
export {provideZoneChangeDetection, NgZoneOptions} from './change_detection/scheduling';
|
||||
export {provideZoneChangeDetection, NgZoneOptions} from './change_detection/scheduling/ng_zone_scheduling';
|
||||
export {enableProdMode, isDevMode} from './util/is_dev_mode';
|
||||
export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, PLATFORM_ID, ANIMATION_MODULE_TYPE, CSP_NONCE} from './application/application_tokens';
|
||||
export {APP_INITIALIZER, ApplicationInitStatus} from './application/application_init';
|
||||
|
||||
@@ -12,6 +12,7 @@ export {IMAGE_CONFIG as ɵIMAGE_CONFIG, IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_
|
||||
export {internalCreateApplication as ɵinternalCreateApplication} from './application/create_application';
|
||||
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
|
||||
export {getEnsureDirtyViewsAreAlwaysReachable as ɵgetEnsureDirtyViewsAreAlwaysReachable, setEnsureDirtyViewsAreAlwaysReachable as ɵsetEnsureDirtyViewsAreAlwaysReachable} from './change_detection/flags';
|
||||
export {ChangeDetectionScheduler as ɵChangeDetectionScheduler} from './change_detection/scheduling/zoneless_scheduling';
|
||||
export {Console as ɵConsole} from './console';
|
||||
export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks} from './defer/discovery';
|
||||
export {renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './defer/instructions';
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {ApplicationInitStatus} from '../application/application_init';
|
||||
import {_callAndReportToErrorHandler, ApplicationRef, BootstrapOptions, compileNgModuleFactory, optionsReducer, remove} from '../application/application_ref';
|
||||
import {getNgZoneOptions, internalProvideZoneChangeDetection, PROVIDED_NG_ZONE} from '../change_detection/scheduling';
|
||||
import {getNgZoneOptions, internalProvideZoneChangeDetection, PROVIDED_NG_ZONE} from '../change_detection/scheduling/ng_zone_scheduling';
|
||||
import {Injectable, InjectionToken, Injector} from '../di';
|
||||
import {ErrorHandler} from '../error_handler';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../errors';
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
|
||||
import {ChangeDetectionScheduler} from '../change_detection/scheduling/zoneless_scheduling';
|
||||
import {Injector} from '../di/injector';
|
||||
import {convertToBitFlags} from '../di/injector_compatibility';
|
||||
import {InjectFlags, InjectOptions} from '../di/interface/injector';
|
||||
@@ -201,6 +202,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
|
||||
const sanitizer = rootViewInjector.get(Sanitizer, null);
|
||||
|
||||
const afterRenderEventManager = rootViewInjector.get(AfterRenderEventManager, null);
|
||||
const changeDetectionScheduler = rootViewInjector.get(ChangeDetectionScheduler, null);
|
||||
|
||||
const environment: LViewEnvironment = {
|
||||
rendererFactory,
|
||||
@@ -208,6 +210,7 @@ export class ComponentFactory<T> extends AbstractComponentFactory<T> {
|
||||
// We don't use inline effects (yet).
|
||||
inlineEffectRunner: null,
|
||||
afterRenderEventManager,
|
||||
changeDetectionScheduler,
|
||||
};
|
||||
|
||||
const hostRenderer = rendererFactory.createRenderer(null, this.componentDef);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {isRootView} from '../interfaces/type_checks';
|
||||
import {FLAGS, LView, LViewFlags} from '../interfaces/view';
|
||||
import {ENVIRONMENT, FLAGS, LView, LViewFlags} from '../interfaces/view';
|
||||
import {getLViewParent} from '../util/view_traversal_utils';
|
||||
|
||||
/**
|
||||
@@ -22,6 +22,7 @@ import {getLViewParent} from '../util/view_traversal_utils';
|
||||
* @returns the root LView
|
||||
*/
|
||||
export function markViewDirty(lView: LView): LView|null {
|
||||
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
|
||||
while (lView) {
|
||||
lView[FLAGS] |= LViewFlags.Dirty;
|
||||
const parent = getLViewParent(lView);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {Renderer, RendererFactory} from './renderer';
|
||||
import {RElement} from './renderer_dom';
|
||||
import {TStylingKey, TStylingRange} from './styling';
|
||||
import {TDeferBlockDetails} from '../../defer/interfaces';
|
||||
import type {ChangeDetectionScheduler} from '../../change_detection/scheduling/zoneless_scheduling';
|
||||
|
||||
|
||||
|
||||
@@ -372,6 +373,9 @@ export interface LViewEnvironment {
|
||||
|
||||
/** Container for after render hooks */
|
||||
afterRenderEventManager: AfterRenderEventManager|null;
|
||||
|
||||
/** Scheduler for change detection to notify when application state changes. */
|
||||
changeDetectionScheduler: ChangeDetectionScheduler|null;
|
||||
}
|
||||
|
||||
/** Flags associated with an LView (saved in LView[FLAGS]) */
|
||||
|
||||
@@ -14,7 +14,7 @@ import {LContainer, LContainerFlags, TYPE} from '../interfaces/container';
|
||||
import {TConstants, TNode} from '../interfaces/node';
|
||||
import {RNode} from '../interfaces/renderer_dom';
|
||||
import {isLContainer, isLView} from '../interfaces/type_checks';
|
||||
import {DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, ON_DESTROY_HOOKS, PARENT, PREORDER_HOOK_FLAGS, PreOrderHookFlags, REACTIVE_TEMPLATE_CONSUMER, TData, TView} from '../interfaces/view';
|
||||
import {DECLARATION_VIEW, ENVIRONMENT, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, ON_DESTROY_HOOKS, PARENT, PREORDER_HOOK_FLAGS, PreOrderHookFlags, REACTIVE_TEMPLATE_CONSUMER, TData, TView} from '../interfaces/view';
|
||||
|
||||
|
||||
|
||||
@@ -207,17 +207,19 @@ export function requiresRefreshOrTraversal(lView: LView) {
|
||||
* parents above.
|
||||
*/
|
||||
export function updateAncestorTraversalFlagsOnAttach(lView: LView) {
|
||||
// When we attach a view that's marked `Dirty`, we should ensure that it is reached during the
|
||||
// next CD traversal so we add the `RefreshView` flag and mark ancestors accordingly.
|
||||
if (lView[FLAGS] & LViewFlags.Dirty && getEnsureDirtyViewsAreAlwaysReachable()) {
|
||||
lView[FLAGS] |= LViewFlags.RefreshView;
|
||||
// TODO(atscott): Simplify if...else cases once getEnsureDirtyViewsAreAlwaysReachable is always
|
||||
// `true`. When we attach a view that's marked `Dirty`, we should ensure that it is reached during
|
||||
// the next CD traversal so we add the `RefreshView` flag and mark ancestors accordingly.
|
||||
if (requiresRefreshOrTraversal(lView)) {
|
||||
markAncestorsForTraversal(lView);
|
||||
} else if (lView[FLAGS] & LViewFlags.Dirty) {
|
||||
if (getEnsureDirtyViewsAreAlwaysReachable()) {
|
||||
lView[FLAGS] |= LViewFlags.RefreshView;
|
||||
markAncestorsForTraversal(lView);
|
||||
} else {
|
||||
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
|
||||
}
|
||||
}
|
||||
|
||||
if (!requiresRefreshOrTraversal(lView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
markAncestorsForTraversal(lView);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,6 +230,7 @@ export function updateAncestorTraversalFlagsOnAttach(lView: LView) {
|
||||
* flag is already `true` or the `lView` is detached.
|
||||
*/
|
||||
export function markAncestorsForTraversal(lView: LView) {
|
||||
lView[ENVIRONMENT].changeDetectionScheduler?.notify();
|
||||
let parent = lView[PARENT];
|
||||
while (parent !== null) {
|
||||
// We stop adding markers to the ancestors once we reach one that already has the marker. This
|
||||
|
||||
@@ -17,9 +17,9 @@ import {checkNoChangesInternal, detectChangesInternal} from './instructions/chan
|
||||
import {markViewDirty} from './instructions/mark_view_dirty';
|
||||
import {CONTAINER_HEADER_OFFSET, VIEW_REFS} from './interfaces/container';
|
||||
import {isLContainer} from './interfaces/type_checks';
|
||||
import {CONTEXT, FLAGS, LView, LViewFlags, PARENT, TVIEW} from './interfaces/view';
|
||||
import {CONTEXT, ENVIRONMENT, FLAGS, LView, LViewFlags, PARENT, TVIEW} from './interfaces/view';
|
||||
import {destroyLView, detachView, detachViewFromDOM} from './node_manipulation';
|
||||
import {storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/view_utils';
|
||||
import {markAncestorsForTraversal, storeLViewOnDestroy, updateAncestorTraversalFlagsOnAttach} from './util/view_utils';
|
||||
|
||||
|
||||
// Needed due to tsickle downleveling where multiple `implements` with classes creates
|
||||
@@ -326,5 +326,6 @@ export class ViewRef<T> implements EmbeddedViewRef<T>, ChangeDetectorRefInterfac
|
||||
ngDevMode && 'This view is already attached to a ViewContainer!');
|
||||
}
|
||||
this._appRef = appRef;
|
||||
updateAncestorTraversalFlagsOnAttach(this._lView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
@@ -1361,6 +1364,9 @@
|
||||
{
|
||||
"name": "unwrapRNode"
|
||||
},
|
||||
{
|
||||
"name": "updateAncestorTraversalFlagsOnAttach"
|
||||
},
|
||||
{
|
||||
"name": "updateMicroTaskStatus"
|
||||
},
|
||||
|
||||
@@ -137,6 +137,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
@@ -1433,6 +1436,9 @@
|
||||
{
|
||||
"name": "unwrapRNode"
|
||||
},
|
||||
{
|
||||
"name": "updateAncestorTraversalFlagsOnAttach"
|
||||
},
|
||||
{
|
||||
"name": "updateMicroTaskStatus"
|
||||
},
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
@@ -1139,6 +1142,9 @@
|
||||
{
|
||||
"name": "unwrapRNode"
|
||||
},
|
||||
{
|
||||
"name": "updateAncestorTraversalFlagsOnAttach"
|
||||
},
|
||||
{
|
||||
"name": "updateMicroTaskStatus"
|
||||
},
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
@@ -1577,6 +1580,9 @@
|
||||
{
|
||||
"name": "init_ng_zone"
|
||||
},
|
||||
{
|
||||
"name": "init_ng_zone_scheduling"
|
||||
},
|
||||
{
|
||||
"name": "init_node"
|
||||
},
|
||||
@@ -1712,9 +1718,6 @@
|
||||
{
|
||||
"name": "init_sanitizer"
|
||||
},
|
||||
{
|
||||
"name": "init_scheduling"
|
||||
},
|
||||
{
|
||||
"name": "init_schema"
|
||||
},
|
||||
@@ -1892,6 +1895,9 @@
|
||||
{
|
||||
"name": "init_zone"
|
||||
},
|
||||
{
|
||||
"name": "init_zoneless_scheduling"
|
||||
},
|
||||
{
|
||||
"name": "initializeDirectives"
|
||||
},
|
||||
|
||||
@@ -95,6 +95,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
|
||||
@@ -98,6 +98,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ComponentFactory"
|
||||
},
|
||||
@@ -890,6 +893,9 @@
|
||||
{
|
||||
"name": "unwrapRNode"
|
||||
},
|
||||
{
|
||||
"name": "updateAncestorTraversalFlagsOnAttach"
|
||||
},
|
||||
{
|
||||
"name": "updateMicroTaskStatus"
|
||||
},
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
@@ -1271,6 +1274,9 @@
|
||||
{
|
||||
"name": "unwrapRNode"
|
||||
},
|
||||
{
|
||||
"name": "updateAncestorTraversalFlagsOnAttach"
|
||||
},
|
||||
{
|
||||
"name": "updateMicroTaskStatus"
|
||||
},
|
||||
|
||||
@@ -101,6 +101,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
@@ -992,6 +995,9 @@
|
||||
{
|
||||
"name": "unwrapRNode"
|
||||
},
|
||||
{
|
||||
"name": "updateAncestorTraversalFlagsOnAttach"
|
||||
},
|
||||
{
|
||||
"name": "updateMicroTaskStatus"
|
||||
},
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
{
|
||||
"name": "ChainedInjector"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionScheduler"
|
||||
},
|
||||
{
|
||||
"name": "ChangeDetectionStrategy"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AsyncPipe} from '@angular/common';
|
||||
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
|
||||
import {afterNextRender, ApplicationRef, ChangeDetectorRef, Component, ComponentRef, createComponent, DebugElement, ElementRef, EnvironmentInjector, ErrorHandler, getDebugNode, inject, Injectable, Input, NgZone, PLATFORM_ID, signal, TemplateRef, Type, ViewChild, ViewContainerRef, ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵNoopNgZone} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
||||
private appRef = inject(ApplicationRef);
|
||||
private _hasPendingChangeDetection = false;
|
||||
get hasPendingChangeDetection() {
|
||||
return this._hasPendingChangeDetection;
|
||||
}
|
||||
|
||||
notify(): void {
|
||||
if (this._hasPendingChangeDetection) return;
|
||||
|
||||
this._hasPendingChangeDetection = true;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.appRef.tick();
|
||||
} finally {
|
||||
this._hasPendingChangeDetection = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe('Angular with NoopNgZone', () => {
|
||||
function nextRender(): Promise<void> {
|
||||
const injector = TestBed.inject(EnvironmentInjector);
|
||||
return new Promise((resolve) => {
|
||||
afterNextRender(resolve, {injector});
|
||||
});
|
||||
}
|
||||
|
||||
async function createAndAttachComponent<T>(type: Type<T>): Promise<ComponentRef<T>> {
|
||||
const environmentInjector = TestBed.inject(EnvironmentInjector);
|
||||
const component = createComponent(type, {environmentInjector});
|
||||
environmentInjector.get(ApplicationRef).attachView(component.hostView);
|
||||
await nextRender();
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('notifies scheduler', () => {
|
||||
let scheduler: ChangeDetectionSchedulerImpl;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: NgZone, useClass: ɵNoopNgZone},
|
||||
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
|
||||
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
|
||||
]
|
||||
});
|
||||
scheduler = TestBed.inject(ChangeDetectionSchedulerImpl);
|
||||
});
|
||||
|
||||
it('when signal updates', async () => {
|
||||
const val = signal('initial');
|
||||
@Component({template: '{{val()}}', standalone: true})
|
||||
class TestComponent {
|
||||
val = val;
|
||||
}
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
val.set('new');
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(component.location.nativeElement.innerText).toEqual('new');
|
||||
});
|
||||
|
||||
it('when using markForCheck()', async () => {
|
||||
@Component({template: '{{val}}', standalone: true})
|
||||
class TestComponent {
|
||||
cdr = inject(ChangeDetectorRef);
|
||||
val = 'initial';
|
||||
setVal(val: string) {
|
||||
this.val = val;
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
component.instance.setVal('new');
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(component.location.nativeElement.innerText).toEqual('new');
|
||||
});
|
||||
|
||||
it('on input binding', async () => {
|
||||
@Component({template: '{{val}}', standalone: true})
|
||||
class TestComponent {
|
||||
@Input() val = 'initial';
|
||||
}
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
component.setInput('val', 'new');
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(component.location.nativeElement.innerText).toEqual('new');
|
||||
});
|
||||
|
||||
it('on event listener bound in template', async () => {
|
||||
@Component({template: '<div (click)="updateVal()">{{val}}</div>', standalone: true})
|
||||
class TestComponent {
|
||||
val = 'initial';
|
||||
|
||||
updateVal() {
|
||||
this.val = 'new';
|
||||
}
|
||||
}
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
getDebugElement(component)
|
||||
.query(p => p.nativeElement.tagName === 'DIV')
|
||||
.triggerEventHandler('click');
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(component.location.nativeElement.innerText).toEqual('new');
|
||||
});
|
||||
|
||||
it('on event listener bound in host', async () => {
|
||||
@Component({host: {'(click)': 'updateVal()'}, template: '{{val}}', standalone: true})
|
||||
class TestComponent {
|
||||
val = 'initial';
|
||||
|
||||
updateVal() {
|
||||
this.val = 'new';
|
||||
}
|
||||
}
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
getDebugElement(component).triggerEventHandler('click');
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(component.location.nativeElement.innerText).toEqual('new');
|
||||
});
|
||||
|
||||
it('with async pipe', async () => {
|
||||
@Component({template: '{{val | async}}', standalone: true, imports: [AsyncPipe]})
|
||||
class TestComponent {
|
||||
val = new BehaviorSubject('initial');
|
||||
}
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
component.instance.val.next('new');
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(component.location.nativeElement.innerText).toEqual('new');
|
||||
});
|
||||
|
||||
it('when creating a view', async () => {
|
||||
@Component({
|
||||
template: '<ng-template #ref>{{"binding"}}</ng-template>',
|
||||
standalone: true,
|
||||
})
|
||||
class TestComponent {
|
||||
@ViewChild(TemplateRef) template!: TemplateRef<unknown>;
|
||||
@ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
|
||||
|
||||
createView(): any {
|
||||
this.viewContainer.createEmbeddedView(this.template);
|
||||
}
|
||||
}
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(false);
|
||||
|
||||
component.instance.createView();
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(component.location.nativeElement.innerText).toEqual('binding');
|
||||
});
|
||||
|
||||
it('when inserting a view', async () => {
|
||||
@Component({
|
||||
template: '{{"binding"}}',
|
||||
standalone: true,
|
||||
})
|
||||
class DynamicCmp {
|
||||
elementRef = inject(ElementRef);
|
||||
}
|
||||
@Component({
|
||||
template: '<ng-template #ref></ng-template>',
|
||||
standalone: true,
|
||||
})
|
||||
class TestComponent {
|
||||
@ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef;
|
||||
}
|
||||
|
||||
const fixture = await createAndAttachComponent(TestComponent);
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(false);
|
||||
|
||||
const component =
|
||||
createComponent(DynamicCmp, {environmentInjector: TestBed.inject(EnvironmentInjector)});
|
||||
fixture.instance.viewContainer.insert(component.hostView);
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(fixture.location.nativeElement.innerText).toEqual('binding');
|
||||
});
|
||||
|
||||
it('when attaching view to ApplicationRef', async () => {
|
||||
@Component({
|
||||
template: '{{"binding"}}',
|
||||
standalone: true,
|
||||
})
|
||||
class DynamicCmp {
|
||||
elementRef = inject(ElementRef);
|
||||
}
|
||||
|
||||
const environmentInjector = TestBed.inject(EnvironmentInjector);
|
||||
const appRef = TestBed.inject(ApplicationRef);
|
||||
const component = createComponent(DynamicCmp, {environmentInjector});
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(false);
|
||||
expect(component.instance.elementRef.nativeElement.innerText).toEqual('');
|
||||
|
||||
appRef.attachView(component.hostView);
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(true);
|
||||
await nextRender();
|
||||
expect(component.instance.elementRef.nativeElement.innerText).toEqual('binding');
|
||||
|
||||
// Don't need to run CD on detach because DOM nodes are just removed
|
||||
// That said, queries need to be updated and currently only update during CD but that's an
|
||||
// unrelated change that needs to happen.
|
||||
appRef.detachView(component.hostView);
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(false);
|
||||
// reattaching non-dirty view does not notify scheduler
|
||||
appRef.attachView(component.hostView);
|
||||
expect(scheduler.hasPendingChangeDetection).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('can recover when an error is re-thrown by the ErrorHandler', async () => {
|
||||
const val = signal('initial');
|
||||
let throwError = false;
|
||||
@Component({template: '{{val()}}{{maybeThrow()}}', standalone: true})
|
||||
class TestComponent {
|
||||
val = val;
|
||||
maybeThrow() {
|
||||
if (throwError) {
|
||||
throw new Error('e');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: NgZone, useClass: ɵNoopNgZone},
|
||||
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
|
||||
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
|
||||
{
|
||||
provide: ErrorHandler, useClass: class extends ErrorHandler {
|
||||
override handleError(error: any): void {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
val.set('new');
|
||||
throwError = true;
|
||||
// error is thrown in a timeout and can't really be "caught".
|
||||
// Still need to wrap in expect so it happens in the expect context and doesn't fail the test.
|
||||
expect(async () => await nextRender()).not.toThrow();
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
throwError = false;
|
||||
await nextRender();
|
||||
expect(component.location.nativeElement.innerText).toEqual('new');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function getDebugElement(component: ComponentRef<unknown>) {
|
||||
return getDebugNode(component.location.nativeElement) as DebugElement;
|
||||
}
|
||||
@@ -147,6 +147,7 @@ describe('di', () => {
|
||||
sanitizer: null,
|
||||
inlineEffectRunner: null,
|
||||
afterRenderEventManager: null,
|
||||
changeDetectionScheduler: null,
|
||||
},
|
||||
{} as any, null, null, null);
|
||||
enterView(contentView);
|
||||
|
||||
@@ -42,8 +42,13 @@ export function enterViewWithOneDiv() {
|
||||
tView.expandoStartIndex = HEADER_OFFSET + 10;
|
||||
const tNode = tView.firstChild = createTNode(tView, null!, TNodeType.Element, 0, 'div', null);
|
||||
const lView = createLView(
|
||||
null, tView, null, LViewFlags.CheckAlways, null, null,
|
||||
{rendererFactory, sanitizer: null, inlineEffectRunner: null, afterRenderEventManager: null},
|
||||
null, tView, null, LViewFlags.CheckAlways, null, null, {
|
||||
rendererFactory,
|
||||
sanitizer: null,
|
||||
inlineEffectRunner: null,
|
||||
afterRenderEventManager: null,
|
||||
changeDetectionScheduler: null
|
||||
},
|
||||
renderer, null, null, null);
|
||||
lView[HEADER_OFFSET] = div;
|
||||
tView.data[HEADER_OFFSET] = tNode;
|
||||
|
||||
@@ -76,6 +76,7 @@ export class ViewFixture {
|
||||
sanitizer: sanitizer || null,
|
||||
afterRenderEventManager: new AfterRenderEventManager(),
|
||||
inlineEffectRunner: null,
|
||||
changeDetectionScheduler: null,
|
||||
},
|
||||
hostRenderer, null, null, null);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user