Skip to content

Commit

Permalink
feat(directives): Add ndcDynamicDirectives directive
Browse files Browse the repository at this point in the history
That allows to apply basic directives on dynamic components

closes #160
  • Loading branch information
gund committed Oct 4, 2018
1 parent 4612702 commit 147189e
Show file tree
Hide file tree
Showing 9 changed files with 886 additions and 8 deletions.
579 changes: 579 additions & 0 deletions src/dynamic/dynamic-directives.directive.spec.ts

Large diffs are not rendered by default.

241 changes: 241 additions & 0 deletions src/dynamic/dynamic-directives.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import {
ChangeDetectorRef,
ComponentRef,
Directive,
DoCheck,
ElementRef,
EventEmitter,
Host,
Inject,
Injector,
Input,
IterableDiffers,
OnDestroy,
OnInit,
Optional,
Output,
SimpleChanges,
Type,
ViewContainerRef,
ViewRef,
} from '@angular/core';

import { COMPONENT_INJECTOR, ComponentInjector } from './component-injector';
import { ComponentOutletInjectorDirective } from './component-outlet-injector.directive';
import { IoFactoryService } from './io-factory.service';
import { InputsType, IoService, OutputsType } from './io.service';
import { getCtorType } from './util';

export interface DynamicDirectiveDef<T> {
type: Type<T>;
inputs?: InputsType;
outputs?: OutputsType;
}

export function dynamicDirectiveDef<T>(
type: Type<T>,
inputs?: InputsType,
outputs?: OutputsType,
): DynamicDirectiveDef<T> {
return { type, inputs, outputs };
}

export interface DirectiveRef<T> {
instance: T;
type: Type<T>;
injector: Injector;
hostComponent: Type<any>;
hostView: ViewRef;
location: ElementRef;
changeDetectorRef: ChangeDetectorRef;
onDestroy: (callback: Function) => void;
}

@Directive({
selector: '[ndcDynamicDirectives],[ngComponentOutletNdcDynamicDirectives]',
})
export class DynamicDirectivesDirective implements OnInit, OnDestroy, DoCheck {
@Input() ndcDynamicDirectives: DynamicDirectiveDef<any>[];
@Input() ngComponentOutletNdcDynamicDirectives: DynamicDirectiveDef<any>[];

@Output() ndcDynamicDirectivesCreated = new EventEmitter<DirectiveRef<any>[]>();

private componentInjector: ComponentInjector = this.injector.get(
this.componentInjectorType,
null,
);

private get directives() {
return this.ndcDynamicDirectives || this.ngComponentOutletNdcDynamicDirectives;
}

private get compInjector() {
return this.componentOutletInjector || this.componentInjector;
}

private get componentRef() {
return this.compInjector.componentRef;
}

private get hostInjector() {
return this.componentRef.injector;
}

private get hostVcr(): ViewContainerRef {
return this.componentRef['_viewRef']['_viewContainerRef'];
}

private dirRef = new Map<Type<any>, DirectiveRef<any>>();
private dirIo = new Map<Type<any>, IoService>();
private dirsDiffer = this.iterableDiffers
.find([])
.create<DynamicDirectiveDef<any>>((_, def) => def.type);
private inputsDiffer = this.iterableDiffers
.find([])
.create<DynamicDirectiveDef<any>>((_, def) => def.inputs);

constructor(
private injector: Injector,
private iterableDiffers: IterableDiffers,
private ioFactoryService: IoFactoryService,
@Inject(COMPONENT_INJECTOR) private componentInjectorType: ComponentInjector,
@Host()
@Optional()
private componentOutletInjector: ComponentOutletInjectorDirective,
) {}

ngOnInit(): void {
if (!this.componentRef) {
throw Error('DynamicDirectivesDirective: ComponentRef not available!');
}
}

ngDoCheck(): void {
this.checkDirectives();
}

ngOnDestroy(): void {
this.dirRef.forEach((dir) => this.destroyDirRef(dir));
this.dirRef.clear();
this.dirIo.clear();
}

private checkDirectives() {
const dirsChanges = this.dirsDiffer.diff(this.directives);

if (!dirsChanges) {
return this.updateDirectives();
}

dirsChanges.forEachRemovedItem(({ item }) => this.destroyDirective(item));

const createdDirs = [];

dirsChanges.forEachAddedItem(({ item }) => createdDirs.push(this.initDirective(item)));

if (createdDirs.length) {
this.ndcDynamicDirectivesCreated.emit(createdDirs.filter(Boolean));
}
}

private updateDirectives() {
this.directives.forEach((dir) => this.updateDirective(dir));
}

private updateDirective(dirDef: DynamicDirectiveDef<any>) {
const io = this.dirIo.get(dirDef.type);
io.update(dirDef.inputs, dirDef.outputs, false, false);
io.maybeUpdate();
}

private initDirective(dirDef: DynamicDirectiveDef<any>): DirectiveRef<any> | undefined {
if (this.dirRef.has(dirDef.type)) {
return;
}

const instance = this.createDirective(dirDef.type);
const dir = {
instance,
type: dirDef.type,
injector: this.hostInjector,
hostComponent: this.componentRef.instance,
hostView: this.componentRef.hostView,
location: this.componentRef.location,
changeDetectorRef: this.componentRef.changeDetectorRef,
onDestroy: this.componentRef.onDestroy,
};

this.callInitHooks(instance);
this.initDirIO(dir, dirDef.inputs, dirDef.outputs);

this.dirRef.set(dir.type, dir);

return dir;
}

private destroyDirective(dirDef: DynamicDirectiveDef<any>) {
if (!this.dirRef.has(dirDef.type)) {
return;
}

this.destroyDirRef(this.dirRef.get(dirDef.type));
this.dirRef.delete(dirDef.type);
this.dirIo.delete(dirDef.type);
}

private initDirIO(dir: DirectiveRef<any>, inputs?: any, outputs?: any) {
const io = this.ioFactoryService.create();
io.init({ componentRef: this.dirToCompDef(dir) }, { trackOutputChanges: true });
io.update(inputs, outputs, !!inputs, !!outputs);
this.dirIo.set(dir.type, io);
}

private dirToCompDef(dir: DirectiveRef<any>): ComponentRef<any> {
return {
...this.componentRef,
destroy: this.componentRef.destroy,
onDestroy: this.componentRef.onDestroy,
instance: dir.instance,
componentType: dir.type,
};
}

private destroyDirRef(dir: DirectiveRef<any>) {
const io = this.dirIo.get(dir.type);
io.ngOnDestroy();

if ('ngOnDestroy' in dir.instance) {
dir.instance.ngOnDestroy();
}
}

private createDirective<T>(dirType: Type<T>): T {
const ctorParams: any[] = getCtorType(dirType);
const resolvedParams = ctorParams.map((p) => this.resolveDep(p));
return new dirType(...resolvedParams);
}

private resolveDep(dep: any): any {
return this.maybeResolveVCR(dep) || this.hostInjector.get(dep);
}

private maybeResolveVCR(dep: any): ViewContainerRef | undefined {
if (dep === ViewContainerRef) {
return this.hostVcr;
}
}

private callInitHooks(obj: any) {
this.callHook(obj, 'ngOnInit');
this.callHook(obj, 'ngAfterContentInit');
this.callHook(obj, 'ngAfterContentChecked');
this.callHook(obj, 'ngAfterViewInit');
this.callHook(obj, 'ngAfterViewChecked');
}

private callHook(obj: any, hook: string, args: any[] = []) {
if (obj[hook]) {
obj[hook](...args);
}
}
}
12 changes: 6 additions & 6 deletions src/dynamic/dynamic.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { CommonModule } from '@angular/common';
import {
ANALYZE_FOR_ENTRY_COMPONENTS,
ModuleWithProviders,
NgModule,
Type,
} from '@angular/core';
import { ANALYZE_FOR_ENTRY_COMPONENTS, ModuleWithProviders, NgModule, Type } from '@angular/core';

import { COMPONENT_INJECTOR, ComponentInjector } from './component-injector';
import { ComponentOutletInjectorDirective } from './component-outlet-injector.directive';
import { DynamicAttributesDirective } from './dynamic-attributes.directive';
import { DynamicDirectivesDirective } from './dynamic-directives.directive';
import { DynamicComponent } from './dynamic.component';
import { DynamicDirective } from './dynamic.directive';
import { IoFactoryService } from './io-factory.service';

@NgModule({
imports: [CommonModule],
Expand All @@ -19,12 +16,14 @@ import { DynamicDirective } from './dynamic.directive';
DynamicDirective,
ComponentOutletInjectorDirective,
DynamicAttributesDirective,
DynamicDirectivesDirective,
],
exports: [
DynamicComponent,
DynamicDirective,
ComponentOutletInjectorDirective,
DynamicAttributesDirective,
DynamicDirectivesDirective,
],
})
export class DynamicModule {
Expand All @@ -41,6 +40,7 @@ export class DynamicModule {
multi: true,
},
{ provide: COMPONENT_INJECTOR, useValue: componentInjector },
IoFactoryService,
],
};
}
Expand Down
1 change: 1 addition & 0 deletions src/dynamic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './dynamic.module';
export * from './dynamic.directive';
export * from './dynamic.component';
export * from './dynamic-attributes.directive';
export * from './dynamic-directives.directive';
export { ComponentInjector } from './component-injector';
16 changes: 16 additions & 0 deletions src/dynamic/io-factory.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* tslint:disable:no-unused-variable */

import { TestBed, async, inject } from '@angular/core/testing';
import { IoFactoryService } from './io-factory.service';

describe('Service: IoFactory', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [IoFactoryService]
});
});

it('should ...', inject([IoFactoryService], (service: IoFactoryService) => {
expect(service).toBeTruthy();
}));
});
12 changes: 12 additions & 0 deletions src/dynamic/io-factory.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ComponentFactoryResolver, Injectable, KeyValueDiffers } from '@angular/core';

import { IoService } from './io.service';

@Injectable()
export class IoFactoryService {
constructor(private differs: KeyValueDiffers, private cfr: ComponentFactoryResolver) {}

create() {
return new IoService(this.differs, this.cfr);
}
}
23 changes: 21 additions & 2 deletions src/dynamic/io.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ import { takeUntil } from 'rxjs/operators';

import { ComponentInjector } from './component-injector';
import { changesFromRecord, createNewChange, noop } from './util';
import { OnDestroy } from '@angular/core';

export type InputsType = { [k: string]: any };
export type OutputsType = { [k: string]: Function };
export type IOMapInfo = { propName: string; templateName: string };
export type IOMappingList = IOMapInfo[];
export type KeyValueChangesAny = KeyValueChanges<any, any>;

export interface IoInitOptions {
trackOutputChanges?: boolean;
}

const recordToChanges = changesFromRecord({ isFirstChanges: true });
const recordToNewChanges = changesFromRecord({ onlyNewChanges: true });

@Injectable()
export class IoService {
export class IoService implements OnDestroy {
private checkInit = this.failInit;

private _lastComponentInst: any = null;
Expand All @@ -34,6 +39,7 @@ export class IoService {
private _inputs: InputsType;
private _outputs: OutputsType;
private _compInjector: ComponentInjector;
private _outputsChanged: (outputs: OutputsType) => boolean = () => false;

private get _compRef() {
return this._compInjector.componentRef;
Expand All @@ -54,9 +60,18 @@ export class IoService {

constructor(private _differs: KeyValueDiffers, private _cfr: ComponentFactoryResolver) {}

init(componentInjector: ComponentInjector) {
ngOnDestroy(): void {
this._disconnectOutputs();
}

init(componentInjector: ComponentInjector, options: IoInitOptions = {}) {
this.checkInit = componentInjector ? noop : this.failInit;
this._compInjector = componentInjector;

if (options.trackOutputChanges) {
const outputsDiffer = this._differs.find({}).create();
this._outputsChanged = (outputs) => !!outputsDiffer.diff(outputs);
}
}

update(
Expand Down Expand Up @@ -92,6 +107,10 @@ export class IoService {
return;
}

if (this._outputsChanged(this._outputs)) {
this.bindOutputs();
}

if (!this._inputs) {
return;
}
Expand Down
Loading

0 comments on commit 147189e

Please sign in to comment.