Skip to content

Commit

Permalink
feat(dynamic-directive): Add support for * syntax with ngComponentOut…
Browse files Browse the repository at this point in the history
…let directive

closes #43 #42
  • Loading branch information
gund committed Aug 11, 2017
1 parent 156b76e commit 2e8b2f9
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 37 deletions.
24 changes: 20 additions & 4 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,26 @@ directive from `@angular/common` instead of `<ndc-dynamic>` and apply `inputs` a
```ts
@Component({
selector: 'my-component',
template: `<div *ngComponentOutlet="component"
[ndcDynamicInputs]="inputs"
[ndcDynamicOutputs]="outputs"
></div>`
template: `<ng-container [ngComponentOutlet]="component"
[ndcDynamicInputs]="inputs"
[ndcDynamicOutputs]="outputs"
></ng-container>`
})
class MyComponent {
component = MyDynamicComponent1;
inputs = {...};
outputs = {...}
}
```

Also you can use `ngComponentOutlet` with `*` syntax:
```ts
@Component({
selector: 'my-component',
template: `<ng-container *ngComponentOutlet="component;
ndcDynamicInputs: inputs;
ndcDynamicOutputs: outputs"
></ng-container>`
})
class MyComponent {
component = MyDynamicComponent1;
Expand Down
135 changes: 120 additions & 15 deletions src/dynamic/dynamic.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { ComponentInjectorComponent, getByPredicate, MockedInjectedComponent, TestComponent } from '../test/index';
import { COMPONENT_INJECTOR } from './component-injector';
import { DynamicDirective } from './dynamic.directive';
import { NgComponentOutlet } from '@angular/common';
import { SimpleChanges } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

import {
ComponentInjectorComponent,
getByPredicate,
InjectedComponent,
MockedInjectedComponent,
TestComponent,
TestModule,
} from '../test/index';
import { COMPONENT_INJECTOR } from './component-injector';
import { DynamicDirective } from './dynamic.directive';

const getComponentInjectorFrom = getByPredicate<ComponentInjectorComponent>(By.directive(ComponentInjectorComponent));
const getInjectedComponentFrom = getByPredicate<InjectedComponent>(By.directive(InjectedComponent));

describe('Directive: Dynamic', () => {
beforeEach(() => {
Expand Down Expand Up @@ -111,30 +120,54 @@ describe('Directive: Dynamic', () => {
});

describe('inputs with `NgComponentOutlet`', () => {
let fixture: ComponentFixture<ComponentInjectorComponent>
, injectedComp: MockedInjectedComponent;
let fixture: ComponentFixture<TestComponent>;

beforeEach(async(() => {
injectedComp = new MockedInjectedComponent();
TestBed.configureTestingModule({
imports: [TestModule],
declarations: [DynamicDirective, TestComponent],
});

const template = `<ng-container [ngComponentOutlet]="comp" [ndcDynamicInputs]="inputs"></ng-container>`;
TestBed.overrideComponent(TestComponent, { set: { template } });
fixture = TestBed.createComponent(TestComponent);

fixture.componentInstance['inputs'] = { prop1: '123', prop2: 1 };
fixture.componentInstance['comp'] = InjectedComponent;
}));

it('should be passed to dynamic component instance', () => {
fixture.detectChanges();

const injectedComp = getInjectedComponentFrom(fixture).component;

expect(injectedComp['prop1']).toBe('123');
expect(injectedComp['prop2']).toBe(1);
});
});

describe('inputs with `NgComponentOutlet` * syntax', () => {
let fixture: ComponentFixture<TestComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, DynamicDirective],
providers: [
// _componentRef is ConmonentRef - so instance should be in `instance` prop
{ provide: NgComponentOutlet, useValue: { _componentRef: { instance: injectedComp } } }
]
imports: [TestModule],
declarations: [DynamicDirective, TestComponent],
});

const template = `<ng-template [ndcDynamicInputs]="inputs"></ng-template>`;
TestBed.overrideComponent(ComponentInjectorComponent, { set: { template } });
fixture = TestBed.createComponent(ComponentInjectorComponent);
const template = `<ng-container *ngComponentOutlet="comp; ndcDynamicInputs: inputs"></ng-container>`;
TestBed.overrideComponent(TestComponent, { set: { template } });
fixture = TestBed.createComponent(TestComponent);

fixture.componentInstance['inputs'] = { prop1: '123', prop2: 1 };
fixture.componentInstance['comp'] = InjectedComponent;
}));

it('should be passed to dynamic component instance', () => {
fixture.detectChanges();

const injectedComp = getInjectedComponentFrom(fixture).component;

expect(injectedComp['prop1']).toBe('123');
expect(injectedComp['prop2']).toBe(1);
});
Expand Down Expand Up @@ -199,4 +232,76 @@ describe('Directive: Dynamic', () => {
expect(tearDownFn).toHaveBeenCalledTimes(1);
});
});

describe('outputs with `NgComponentOutlet`', () => {
let fixture: ComponentFixture<TestComponent>
, outputSpy: jasmine.Spy;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TestModule],
declarations: [DynamicDirective, TestComponent],
});

const template = `<ng-container [ngComponentOutlet]="comp" [ndcDynamicOutputs]="outputs"></ng-container>`;
TestBed.overrideComponent(TestComponent, { set: { template } });
fixture = TestBed.createComponent(TestComponent);

outputSpy = jasmine.createSpy('outputSpy');

InjectedComponent.prototype['onEvent'] = new Subject<any>();

fixture.componentInstance['outputs'] = { onEvent: outputSpy };
fixture.componentInstance['comp'] = InjectedComponent;
}));

afterEach(() => delete InjectedComponent.prototype['onEvent']);

it('should be passed to dynamic component instance', () => {
fixture.detectChanges();

const injectedComp = getInjectedComponentFrom(fixture).component;

injectedComp['onEvent'].next('data');

expect(outputSpy).toHaveBeenCalledTimes(1);
expect(outputSpy).toHaveBeenCalledWith('data');
});
});

describe('outputs with `NgComponentOutlet` * syntax', () => {
let fixture: ComponentFixture<TestComponent>
, outputSpy: jasmine.Spy;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TestModule],
declarations: [DynamicDirective, TestComponent],
});

const template = `<ng-container *ngComponentOutlet="comp; ndcDynamicOutputs: outputs"></ng-container>`;
TestBed.overrideComponent(TestComponent, { set: { template } });
fixture = TestBed.createComponent(TestComponent);

outputSpy = jasmine.createSpy('outputSpy');

InjectedComponent.prototype['onEvent'] = new Subject<any>();

fixture.componentInstance['outputs'] = { onEvent: outputSpy };
fixture.componentInstance['comp'] = InjectedComponent;
}));

afterEach(() => delete InjectedComponent.prototype['onEvent']);

it('should be passed to dynamic component instance', () => {
fixture.detectChanges();

const injectedComp = getInjectedComponentFrom(fixture).component;

injectedComp['onEvent'].next('data');

expect(outputSpy).toHaveBeenCalledTimes(1);
expect(outputSpy).toHaveBeenCalledWith('data');
});
});
});
64 changes: 46 additions & 18 deletions src/dynamic/dynamic.directive.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { COMPONENT_INJECTOR, ComponentInjector } from './component-injector';
import { CustomSimpleChange, UNINITIALIZED } from './custom-simple-change';
import { NgComponentOutlet } from '@angular/common';
import {
Directive,
DoCheck,
Host,
Inject,
Injector,
Input,
Expand All @@ -12,28 +11,43 @@ import {
OnChanges,
OnDestroy,
Optional,
SimpleChanges
SimpleChanges,
SkipSelf,
TemplateRef,
} from '@angular/core';
import { Subject } from 'rxjs/Subject';

import { COMPONENT_INJECTOR, ComponentInjector } from './component-injector';
import { CustomSimpleChange, UNINITIALIZED } from './custom-simple-change';

export type KeyValueChangeRecordAny = KeyValueChangeRecord<any, any>;

@Directive({
selector: '[ndcDynamicInputs], [ndcDynamicOutputs]'
selector: '[ndcDynamicInputs],[ndcDynamicOutputs],[ngComponentOutletNdcDynamicInputs],[ngComponentOutletNdcDynamicOutputs]'
})
export class DynamicDirective implements OnChanges, DoCheck, OnDestroy {

@Input() ndcDynamicInputs: { [k: string]: any } = {};
@Input() ndcDynamicOutputs: { [k: string]: Function } = {};
@Input() ndcDynamicInputs: { [k: string]: any };
@Input() ngComponentOutletNdcDynamicInputs: { [k: string]: any };
@Input() ndcDynamicOutputs: { [k: string]: Function };
@Input() ngComponentOutletNdcDynamicOutputs: { [k: string]: Function };

private _componentInjector: ComponentInjector = this._injector.get(this._componentInjectorType, {});
private _lastComponentInst: any = this._componentInjector;
private _lastInputChanges: SimpleChanges;
private _inputsDiffer = this._differs.find(this.ndcDynamicInputs).create(null as any);
private _inputsDiffer = this._differs.find({}).create(null as any);
private _destroyed$ = new Subject<void>();

private get _inputs() {
return this.ndcDynamicInputs || this.ngComponentOutletNdcDynamicInputs;
}

private get _outputs() {
return this.ndcDynamicOutputs || this.ngComponentOutletNdcDynamicOutputs;
}

private get _compOutletInst(): any {
return this._componentOutlet && (<any>this._componentOutlet)._componentRef && (<any>this._componentOutlet)._componentRef.instance;
return this._extractCompFrom(this._componentOutlet);
}

private get _componentInst(): any {
Expand All @@ -54,7 +68,7 @@ export class DynamicDirective implements OnChanges, DoCheck, OnDestroy {
private _differs: KeyValueDiffers,
private _injector: Injector,
@Inject(COMPONENT_INJECTOR) private _componentInjectorType: ComponentInjector,
@Optional() private _componentOutlet: NgComponentOutlet
@Host() @Optional() private _componentOutlet: NgComponentOutlet,
) { }

ngOnChanges(changes: SimpleChanges) {
Expand All @@ -71,7 +85,13 @@ export class DynamicDirective implements OnChanges, DoCheck, OnDestroy {
return;
}

const inputsChanges = this._inputsDiffer.diff(this.ndcDynamicInputs);
const inputs = this._inputs;

if (!inputs) {
return;
}

const inputsChanges = this._inputsDiffer.diff(inputs);

if (inputsChanges) {
const isNotFirstChange = !!this._lastInputChanges;
Expand All @@ -88,28 +108,31 @@ export class DynamicDirective implements OnChanges, DoCheck, OnDestroy {
}

updateInputs(isFirstChange = false) {
if (!this.ndcDynamicInputs || !this._componentInst) {
const inputs = this._inputs;

if (!inputs || !this._componentInst) {
return;
}

Object.keys(this.ndcDynamicInputs).forEach(p =>
this._componentInst[p] = this.ndcDynamicInputs[p]);
Object.keys(inputs).forEach(p =>
this._componentInst[p] = inputs[p]);

this.notifyOnInputChanges(this._lastInputChanges, isFirstChange);
}

bindOutputs() {
this._destroyed$.next();
const outputs = this._outputs;

if (!this.ndcDynamicOutputs || !this._componentInst) {
if (!outputs || !this._componentInst) {
return;
}

Object.keys(this.ndcDynamicOutputs)
Object.keys(outputs)
.filter(p => this._componentInst[p])
.forEach(p => this._componentInst[p]
.takeUntil(this._destroyed$)
.subscribe(this.ndcDynamicOutputs[p]));
.subscribe(outputs[p]));
}

notifyOnInputChanges(changes: SimpleChanges = {}, forceFirstChanges: boolean) {
Expand All @@ -127,9 +150,10 @@ export class DynamicDirective implements OnChanges, DoCheck, OnDestroy {

private _collectFirstChanges(): SimpleChanges {
const changes = {} as SimpleChanges;
const inputs = this._inputs;

Object.keys(this.ndcDynamicInputs).forEach(prop =>
changes[prop] = new CustomSimpleChange(UNINITIALIZED, this.ndcDynamicInputs[prop], true));
Object.keys(inputs).forEach(prop =>
changes[prop] = new CustomSimpleChange(UNINITIALIZED, inputs[prop], true));

return changes;
}
Expand All @@ -146,4 +170,8 @@ export class DynamicDirective implements OnChanges, DoCheck, OnDestroy {
return changes;
}

private _extractCompFrom(outlet: NgComponentOutlet | null): any {
return outlet && (<any>outlet)._componentRef && (<any>outlet)._componentRef.instance;
}

}

0 comments on commit 2e8b2f9

Please sign in to comment.