diff --git a/.gitignore b/.gitignore index 48c1e77..ce05b7c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ # IDE - VSCode .vscode/* -!.vscode/settings.json +.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json diff --git a/packages/core/change-detection-scheduler/README.md b/packages/core/change-detection-scheduler/README.md new file mode 100644 index 0000000..e049512 --- /dev/null +++ b/packages/core/change-detection-scheduler/README.md @@ -0,0 +1,32 @@ +# Auto Change Detection with NoopZone + +Automatically trigger change detection without Zone.js. + +## Type + +**Enhancement** + +## Provenance + ++ https://github.com/angular/angular/issues/20204 + +## NgModule + +`@angular-contrib/core#ChangeDetectionSchedulerModule` + +## Usage + +```typescript +import { ChangeDetectionSchedulerModule } from '@angular-contrib/core'; + +@NgModule({ + imports: [ ChangeDetectionSchedulerModule ], +}) +class MyModule { } + +platformBrowserDynamic.bootstrapModule(MyModule, { ngZone: 'noop' }); +``` + +## Note + ++ Depends on `markForCheck` integration like event binding, async pipe; diff --git a/packages/core/change-detection-scheduler/change-detection-scheduler.module.ts b/packages/core/change-detection-scheduler/change-detection-scheduler.module.ts new file mode 100644 index 0000000..56a9207 --- /dev/null +++ b/packages/core/change-detection-scheduler/change-detection-scheduler.module.ts @@ -0,0 +1,19 @@ +import { ComponentFactoryResolver, Injector, NgModule, RendererFactory2 } from '@angular/core'; +import { ChangeDetectionScheduler, ChangeDetectionScheduler_, ChangeDetectionSchedulerInitializer } from './change-detection-scheduler'; + +@NgModule({ + declarations: [ ChangeDetectionSchedulerInitializer ], + entryComponents: [ ChangeDetectionSchedulerInitializer ], + providers: [ + { provide: ChangeDetectionScheduler, useClass: ChangeDetectionScheduler_ }, + ], +}) +export class ChangeDetectionSchedulerModule { + constructor(cfResolver: ComponentFactoryResolver, injector: Injector, rendererFactory: RendererFactory2) { + const componentFactory = cfResolver.resolveComponentFactory(ChangeDetectionSchedulerInitializer); + const renderer = rendererFactory.createRenderer(null, null); + const host = renderer.createElement('div'); + const componentRef = componentFactory.create(injector, undefined, host); + componentRef.destroy(); + } +} diff --git a/packages/core/change-detection-scheduler/change-detection-scheduler.spec.ts b/packages/core/change-detection-scheduler/change-detection-scheduler.spec.ts new file mode 100644 index 0000000..b88579a --- /dev/null +++ b/packages/core/change-detection-scheduler/change-detection-scheduler.spec.ts @@ -0,0 +1,64 @@ +import { ChangeDetectorRef, Component, EventEmitter, NgZone } from '@angular/core'; +import { async, inject, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionScheduler } from './change-detection-scheduler'; +import { ChangeDetectionSchedulerModule } from './change-detection-scheduler.module'; + +describe('ChangeDetectionScheduler', () => { + let fixture: ComponentFixture; + let component: TestComponent; + + let mockScheduler: ChangeDetectionScheduler; + + const dummyNoopNgZone: NgZone = { + hasPendingMicrotasks: false, + hasPendingMacrotasks: false, + isStable: true, + onUnstable: new EventEmitter(), + onMicrotaskEmpty: new EventEmitter(), + onStable: new EventEmitter(), + onError: new EventEmitter(), + run(fn: () => any): any { return fn(); }, + runGuarded(fn: () => any): any { return fn(); }, + runOutsideAngular(fn: () => any): any { return fn(); }, + runTask(fn: () => any): any { return fn(); }, + }; + + beforeEach(() => { + mockScheduler = { + schedule: jasmine.createSpy('schedule'), + }; + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [ChangeDetectionSchedulerModule], + providers: [ + { provide: NgZone, useValue: dummyNoopNgZone }, + { provide: ChangeDetectionScheduler, useValue: mockScheduler }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + }); + + it('should perform change detection in startup', inject([ChangeDetectionScheduler], (scheduler: ChangeDetectionScheduler) => { + expect(scheduler.schedule).toHaveBeenCalledTimes(1); + })); + + it('should perform change detection after events', inject([ChangeDetectionScheduler], (scheduler: ChangeDetectionScheduler) => { + component.changeDetectorRef.markForCheck(); + expect(scheduler.schedule).toHaveBeenCalledTimes(2); + })); +}); + +@Component({ + selector: 'test-cmp', + template: ``, +}) +class TestComponent { + constructor(public changeDetectorRef: ChangeDetectorRef) { } +} diff --git a/packages/core/change-detection-scheduler/change-detection-scheduler.ts b/packages/core/change-detection-scheduler/change-detection-scheduler.ts new file mode 100644 index 0000000..2a7b1e6 --- /dev/null +++ b/packages/core/change-detection-scheduler/change-detection-scheduler.ts @@ -0,0 +1,46 @@ +import { ApplicationRef, ChangeDetectorRef, Component, Injectable, NgZone } from '@angular/core'; + +let tickScheduled = false; + +export abstract class ChangeDetectionScheduler { + abstract schedule(): void; +} + +@Injectable() +export class ChangeDetectionScheduler_ implements ChangeDetectionScheduler { + constructor(private appRef: ApplicationRef) { } + + schedule(): void { + tickScheduled = true; + Promise.resolve(null).then(() => { + tickScheduled = false; + this.appRef.tick(); + }); + } +} + +@Component({ + template: '', +}) +export class ChangeDetectionSchedulerInitializer { + constructor(appRef: ApplicationRef, cdRef: ChangeDetectorRef, ngZone: NgZone, scheduler: ChangeDetectionScheduler) { + const markForCheck = cdRef.markForCheck; + + const scheduleTickIfNeeded = () => { + if (!(ngZone instanceof NgZone) && !tickScheduled) { + scheduler.schedule(); + } + }; + + while (typeof cdRef === 'object' && !cdRef.hasOwnProperty('markForCheck') && cdRef.constructor.prototype) { + cdRef = cdRef.constructor.prototype; + } + + cdRef.markForCheck = function () { + markForCheck.call(this); + scheduleTickIfNeeded(); + }; + + scheduleTickIfNeeded(); + } +} diff --git a/packages/core/change-detection-scheduler/index.ts b/packages/core/change-detection-scheduler/index.ts new file mode 100644 index 0000000..8ed78c8 --- /dev/null +++ b/packages/core/change-detection-scheduler/index.ts @@ -0,0 +1,2 @@ +export * from './change-detection-scheduler'; +export * from './change-detection-scheduler.module'; diff --git a/packages/core/index.ts b/packages/core/index.ts index 7bbfb9c..2edd5f8 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -1,2 +1,3 @@ +export * from './change-detection-scheduler/index'; export * from './iterable-differs/index'; export * from './key-value-differs/index'; diff --git a/packages/core/iterable-differs/iterable-differs.module.ts b/packages/core/iterable-differs/iterable-differs.module.ts index 337fd97..d387f32 100644 --- a/packages/core/iterable-differs/iterable-differs.module.ts +++ b/packages/core/iterable-differs/iterable-differs.module.ts @@ -1,4 +1,4 @@ -import { Inject, IterableDiffers, IterableDifferFactory, ModuleWithProviders, NgModule } from '@angular/core'; +import { Inject, IterableDiffers, IterableDifferFactory, NgModule } from '@angular/core'; import { ITERABLE_DIFFER_FACTORIES } from './iterable-differs'; @NgModule() diff --git a/packages/core/key-value-differs/key-value-differs.module.ts b/packages/core/key-value-differs/key-value-differs.module.ts index 8cd8d14..7a3ab89 100644 --- a/packages/core/key-value-differs/key-value-differs.module.ts +++ b/packages/core/key-value-differs/key-value-differs.module.ts @@ -1,4 +1,4 @@ -import { Inject, KeyValueDiffers, KeyValueDifferFactory, ModuleWithProviders, NgModule } from '@angular/core'; +import { Inject, KeyValueDiffers, KeyValueDifferFactory, NgModule } from '@angular/core'; import { KEY_VALUE_DIFFER_FACTORIES } from './key-value-differs'; @NgModule() diff --git a/tslint.json b/tslint.json index abc8299..cc27edc 100644 --- a/tslint.json +++ b/tslint.json @@ -5,7 +5,7 @@ "rules": { "arrow-return-shorthand": true, "callable-types": true, - "class-name": true, + "class-name": false, "comment-format": [ true, "check-space"