diff --git a/package.json b/package.json index 88d4841..e52b660 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ngx-context", - "version": "1.1.0", + "version": "1.1.1", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/lib/consumer.abstract.ts b/src/lib/consumer.abstract.ts index 6e978c5..af9738f 100644 --- a/src/lib/consumer.abstract.ts +++ b/src/lib/consumer.abstract.ts @@ -1,17 +1,18 @@ import { ChangeDetectorRef, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { Subject } from 'rxjs'; -import { filter, takeUntil } from 'rxjs/operators'; +import { filter, startWith, takeUntil } from 'rxjs/operators'; import { parseKeys } from './internals'; import { ContextProviderComponent } from './provider.component'; import { ContextMap } from './symbols'; export abstract class AbstractContextConsumer implements OnChanges, OnDestroy, OnInit { - protected consumed = new Map(); protected destroy$ = new Subject(); protected initialized: boolean; protected _contextMap = {}; protected _consume: string | string[] = ''; + consumed = new Map(); + @Input() set contextMap(map: ContextMap) { this._contextMap = map || {}; @@ -48,6 +49,7 @@ export abstract class AbstractContextConsumer implements OnChanges, OnDestroy this.provider.change$ .pipe( takeUntil(this.destroy$), + startWith(...Array.from(this.provider.provided.keys())), filter(key => !!key), ) .subscribe(providerKey => this.syncProperties(consumed, providerKey)); diff --git a/src/lib/disposer.directive.ts b/src/lib/disposer.directive.ts index 8d822ee..1c977a6 100644 --- a/src/lib/disposer.directive.ts +++ b/src/lib/disposer.directive.ts @@ -2,20 +2,26 @@ import { Directive, EmbeddedViewRef, Input, + OnChanges, + OnDestroy, Optional, SkipSelf, TemplateRef, ViewContainerRef, } from '@angular/core'; import { Subject } from 'rxjs'; -import { filter, takeUntil } from 'rxjs/operators'; +import { filter, startWith, takeUntil } from 'rxjs/operators'; import { parseKeys } from './internals'; import { ContextProviderComponent } from './provider.component'; +export class Context { + $implicit: { [key: string]: any } = {}; +} + @Directive({ selector: '[contextDisposer]', }) -export class ContextDisposerDirective { +export class ContextDisposerDirective implements OnChanges, OnDestroy { private destroy$ = new Subject(); private _dispose: string | string[] = ''; private view: EmbeddedViewRef; @@ -49,6 +55,7 @@ export class ContextDisposerDirective { this.provider.change$ .pipe( takeUntil(this.destroy$), + startWith(...Array.from(this.provider.provided.keys())), filter(key => !!key), ) .subscribe(providerKey => this.syncProperties(disposed, providerKey)); @@ -83,7 +90,3 @@ export class ContextDisposerDirective { if (this.view) this.vcRef.clear(); } } - -export class Context { - $implicit: { [key: string]: any } = {}; -} diff --git a/src/lib/provider.component.ts b/src/lib/provider.component.ts index f0ad3ee..e07027d 100644 --- a/src/lib/provider.component.ts +++ b/src/lib/provider.component.ts @@ -21,10 +21,11 @@ import { ContextMap } from './symbols'; }) export class ContextProviderComponent implements OnChanges, OnInit { private initialized = false; - private provided = new Map(); private _contextMap: ContextMap = {}; private _provide: string | string[] = ''; + provided = new Map(); + @Input() set contextMap(map: ContextMap) { this._contextMap = map || {}; diff --git a/src/package.json b/src/package.json index 7dccc33..dd02133 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "ngx-context", - "version": "1.1.0", + "version": "1.1.1", "repository": { "type": "git", "url": "git+https://github.com/ng-turkey/ngx-context.git" diff --git a/src/tests/disposer.directive.spec.ts b/src/tests/disposer.directive.spec.ts index 9e81499..a98e43e 100644 --- a/src/tests/disposer.directive.spec.ts +++ b/src/tests/disposer.directive.spec.ts @@ -1,11 +1,5 @@ import { ChangeDetectorRef } from '@angular/core'; -import { - ComponentFixture, - fakeAsync, - inject, - TestBed, - tick, -} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ContextDisposerDirective } from '../lib/disposer.directive'; import { ContextProviderComponent } from '../lib/provider.component'; import { TestDisposerComponent } from './test-disposer.component'; diff --git a/src/tests/integration.spec.ts b/src/tests/integration.spec.ts index 42da800..e9663a3 100644 --- a/src/tests/integration.spec.ts +++ b/src/tests/integration.spec.ts @@ -8,6 +8,7 @@ import { NgxContextModule } from '../lib/context.module'; import { ContextDisposerDirective } from '../lib/disposer.directive'; import { TestConsumerComponent } from './test-consumer.component'; import { TestDisposerComponent } from './test-disposer.component'; +import { TestDummyComponent } from './test-dummy.component'; import { TestFormComponent } from './test-form.component'; import { TestMiddleComponent } from './test-middle.component'; import { TestProviderComponent } from './test-provider.component'; @@ -84,8 +85,10 @@ describe('Context Provider & Consumer', function(this: IContextConsumer) { }).compileComponents(); this.fixture = TestBed.createComponent(TestProviderComponent); + const props = ['target', 'title']; - shouldSyncProvidedProperty.bind(this, 'target'); + setProvidedProperties.call(this, props); + shouldSyncProvidedProperties.call(this, props); })); it('should work through router outlet', fakeAsync(() => { @@ -104,34 +107,66 @@ describe('Context Provider & Consumer', function(this: IContextConsumer) { RouterTestingModule.withRoutes([ { path: '', + pathMatch: 'full', + component: TestDummyComponent, + }, + { + path: 'test', component: TestMiddleComponent, }, ]), NgxContextModule, ], - declarations: [TestProviderComponent, TestMiddleComponent, TestConsumerComponent], + declarations: [ + TestProviderComponent, + TestDummyComponent, + TestMiddleComponent, + TestConsumerComponent, + ], }).compileComponents(); - this.fixture = TestBed.createComponent(TestProviderComponent); + const props = ['target', 'title']; + setProvidedProperties.call(this, props); this.fixture.ngZone.run(() => { // Navigate to base path - this.fixture.debugElement.injector.get(Router).initialNavigation(); + const router = this.fixture.debugElement.injector.get(Router); + router.initialNavigation(); + tick(); + + // TestMiddleComponent should not be loaded at first + expect( + this.fixture.debugElement.query(By.directive(TestMiddleComponent)), + ).toBeNull(); + + router.navigate(['test']); tick(); - shouldSyncProvidedProperty.bind(this, 'target'); + // All properties should be synced at start + shouldSyncProvidedProperties.call(this, props); + + // And properties should be kept in sync later + this.parent.title = null; + this.fixture.detectChanges(); + expect(this.child.title).toBeNull(); }); })); }); type Excluded = 'provided' | 'contextMap' | 'consume'; +type Props = Array< + Exclude +>; -function shouldSyncProvidedProperty( - this: IContextConsumer, - prop: Exclude, -): void { - // Query component instances +function setProvidedProperties(this: IContextConsumer, props: Props): void { + this.fixture = TestBed.createComponent(TestProviderComponent); this.parent = this.fixture.debugElement.componentInstance; + this.parent.provided = props; + this.fixture.detectChanges(); +} + +function shouldSyncProvidedProperties(this: IContextConsumer, props: Props): void { + // Query component instances this.middle = this.fixture.debugElement.query( By.directive(TestMiddleComponent), ).componentInstance; @@ -139,15 +174,21 @@ function shouldSyncProvidedProperty( By.directive(ContextConsumerDirective), ).componentInstance; - expect(this.child[prop]).not.toBe(this.parent[prop]); + // Confirm the starting value is different + props.forEach(prop => { + if (typeof this.parent[prop] !== 'undefined') + expect(this.child[prop]).not.toEqual(this.parent[prop]); + }); // Provide property - this.parent.provided = prop; - this.middle.provided = prop; + this.middle.provided = props; // Detect changes this.fixture.detectChanges(); tick(); - expect(this.child[prop]).toBe(this.parent[prop]); + props.forEach(prop => { + expect(this.child[prop]).not.toBeUndefined(); + expect(this.child[prop]).toEqual(this.parent[prop]); + }); } diff --git a/src/tests/test-dummy.component.ts b/src/tests/test-dummy.component.ts new file mode 100644 index 0000000..679646d --- /dev/null +++ b/src/tests/test-dummy.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'context-test-dummy', + template: ``, +}) +export class TestDummyComponent {} diff --git a/src/tests/test-provider.component.ts b/src/tests/test-provider.component.ts index 327acd3..90db32a 100644 --- a/src/tests/test-provider.component.ts +++ b/src/tests/test-provider.component.ts @@ -9,7 +9,7 @@ import { ContextMap } from '../lib/symbols'; encapsulation: ViewEncapsulation.None, }) export class TestProviderComponent { - private _title: string; + private _title = 'Testing'; greeting$ = of('Hello'); target = 'World';