-
-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
1,005 additions
and
33 deletions.
There are no files selected for viewing
66 changes: 66 additions & 0 deletions
66
projects/ng-dynamic-component/src/lib/dynamic-io-v2/dynamic-io-v2.directive.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
/* eslint-disable @angular-eslint/no-conflicting-lifecycle */ | ||
import { | ||
Directive, | ||
DoCheck, | ||
Inject, | ||
Input, | ||
OnChanges, | ||
SimpleChanges, | ||
} from '@angular/core'; | ||
import { | ||
DynamicComponentInjector, | ||
DynamicComponentInjectorToken, | ||
} from '../component-injector'; | ||
import { IoService } from '../io'; | ||
import { IoAdapterService } from '../io/io-adapter.service'; | ||
import { IOData } from '../io/io-data'; | ||
import { TemplateParser, TemplateTokeniser } from '../template'; | ||
|
||
@Directive({ | ||
selector: '[ndcDynamicIo]', | ||
exportAs: 'ndcDynamicIo', | ||
providers: [IoService, IoAdapterService], | ||
}) | ||
export class DynamicIoV2Directive implements DoCheck, OnChanges { | ||
@Input() | ||
ndcDynamicIo?: IOData | string | null; | ||
|
||
private get componentInst(): Record<string, unknown> { | ||
return ( | ||
(this.compInjector.componentRef?.instance as Record<string, unknown>) ?? | ||
{} | ||
); | ||
} | ||
|
||
constructor( | ||
private ioService: IoAdapterService, | ||
@Inject(DynamicComponentInjectorToken) | ||
private compInjector: DynamicComponentInjector, | ||
) {} | ||
|
||
async ngOnChanges(changes: SimpleChanges) { | ||
if (changes['ndcDynamicIo'] && typeof this.ndcDynamicIo === 'string') { | ||
this.updateIo(await this.strToIo(this.ndcDynamicIo)); | ||
} | ||
} | ||
|
||
ngDoCheck() { | ||
if (typeof this.ndcDynamicIo !== 'string') { | ||
this.updateIo(this.ndcDynamicIo); | ||
} | ||
} | ||
|
||
private async updateIo(io?: IOData | null) { | ||
this.ioService.update(io); | ||
} | ||
|
||
private strToIo(ioStr: string) { | ||
const tokeniser = new TemplateTokeniser(); | ||
const parser = new TemplateParser(tokeniser, this.componentInst); | ||
const ioPromise = parser.getIo(); | ||
|
||
tokeniser.feed(ioStr); | ||
|
||
return ioPromise; | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
projects/ng-dynamic-component/src/lib/dynamic-io-v2/dynamic-io-v2.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { CommonModule } from '@angular/common'; | ||
import { NgModule } from '@angular/core'; | ||
|
||
import { ComponentOutletInjectorModule } from '../component-outlet'; | ||
import { DynamicIoV2Directive } from './dynamic-io-v2.directive'; | ||
|
||
/** | ||
* @public | ||
*/ | ||
@NgModule({ | ||
imports: [CommonModule], | ||
exports: [DynamicIoV2Directive, ComponentOutletInjectorModule], | ||
declarations: [DynamicIoV2Directive], | ||
}) | ||
export class DynamicIoV2Module {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './dynamic-io-v2.directive'; | ||
export * from './dynamic-io-v2.module'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
projects/ng-dynamic-component/src/lib/io/io-adapter.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import { Inject, Injectable, KeyValueDiffers } from '@angular/core'; | ||
import { | ||
DynamicComponentInjector, | ||
DynamicComponentInjectorToken, | ||
} from '../component-injector'; | ||
import { IOData } from './io-data'; | ||
import { IoService } from './io.service'; | ||
import { EventHandler, InputsType, OutputsType } from './types'; | ||
|
||
@Injectable() | ||
export class IoAdapterService { | ||
private ioDiffer = this.differs.find({}).create(); | ||
|
||
private inputs: InputsType = {}; | ||
private outputs: OutputsType = {}; | ||
|
||
private get componentInst(): Record<string, unknown> { | ||
return ( | ||
(this.compInjector.componentRef?.instance as Record<string, unknown>) ?? | ||
{} | ||
); | ||
} | ||
|
||
constructor( | ||
private differs: KeyValueDiffers, | ||
private ioService: IoService, | ||
@Inject(DynamicComponentInjectorToken) | ||
private compInjector: DynamicComponentInjector, | ||
) {} | ||
|
||
update(io?: IOData | null): void { | ||
if (!io) { | ||
io = {}; | ||
} | ||
|
||
const ioChanges = this.ioDiffer.diff(io); | ||
|
||
if (!ioChanges) { | ||
return; | ||
} | ||
|
||
ioChanges.forEachRemovedItem((record) => { | ||
const name = this.getIOName(record.key); | ||
delete this.inputs[name]; | ||
delete this.outputs[name]; | ||
}); | ||
|
||
ioChanges.forEachAddedItem((record) => { | ||
this.updateProp(record.key, record.currentValue); | ||
}); | ||
|
||
ioChanges.forEachChangedItem((record) => { | ||
this.updateProp(record.key, record.currentValue); | ||
}); | ||
|
||
this.ioService.update(this.inputs, this.outputs); | ||
} | ||
|
||
private getIOName(prop: string) { | ||
if (prop.startsWith('[') || prop.startsWith('(')) { | ||
return prop.slice(1, -1); | ||
} | ||
|
||
if (prop.startsWith('[(')) { | ||
return prop.slice(2, -2); | ||
} | ||
|
||
return prop; | ||
} | ||
|
||
private updateProp(prop: string, data: unknown) { | ||
if (this.maybeInputBind(prop, data, this.inputs)) { | ||
return; | ||
} | ||
|
||
if (this.maybeOutput(prop, data, this.outputs)) { | ||
return; | ||
} | ||
|
||
if (this.maybeInput2W(prop, data, this.inputs, this.outputs)) { | ||
return; | ||
} | ||
|
||
if (this.maybeInputProp(prop, data, this.inputs)) { | ||
return; | ||
} | ||
|
||
throw new Error(`IoAdapterService: Unknown binding type '${prop}!'`); | ||
} | ||
|
||
private maybeInputBind(prop: string, data: unknown, record: InputsType) { | ||
if (!prop.startsWith('[') || !prop.endsWith(']')) { | ||
return false; | ||
} | ||
|
||
const name = prop.slice(1, -1); | ||
|
||
if (typeof data === 'string' && data in this.componentInst) { | ||
this.addPropGetter(record, name); | ||
return true; | ||
} | ||
|
||
try { | ||
if (typeof data === 'string') { | ||
data = JSON.parse(data); | ||
} | ||
} catch { | ||
throw new Error( | ||
`Input binding must be a string or valid JSON string but given ${typeof data}!`, | ||
); | ||
} | ||
|
||
record[name] = data; | ||
|
||
return true; | ||
} | ||
|
||
private maybeInputProp(prop: string, data: unknown, inputs: InputsType) { | ||
if (typeof data !== 'string') { | ||
throw new Error(`Input binding should be a string!`); | ||
} | ||
|
||
inputs[prop] = data; | ||
|
||
return true; | ||
} | ||
|
||
private maybeInput2W( | ||
prop: string, | ||
data: unknown, | ||
inputs: InputsType, | ||
outputs: OutputsType, | ||
) { | ||
if (!prop.startsWith('[(') || !prop.endsWith(')]')) { | ||
return false; | ||
} | ||
|
||
if (typeof data !== 'string') { | ||
throw new Error(`Two-way binding must be a string!`); | ||
} | ||
|
||
const input = prop.slice(2, -2); | ||
const output = `${input}Change`; | ||
|
||
this.addPropGetter(inputs, input, data); | ||
|
||
outputs[output] = (value) => void (this.componentInst[data] = value); | ||
|
||
return true; | ||
} | ||
|
||
private maybeOutput(prop: string, data: unknown, record: OutputsType) { | ||
if (!prop.startsWith('(') || !prop.endsWith(')')) { | ||
return false; | ||
} | ||
|
||
const name = prop.slice(1, -1); | ||
|
||
if (typeof data === 'string' && data in this.componentInst) { | ||
this.addPropGetter(record, name); | ||
return true; | ||
} | ||
|
||
if (typeof data !== 'function') { | ||
throw new Error(`Output binding must be function or method name!`); | ||
} | ||
|
||
record[name] = data as EventHandler; | ||
|
||
return true; | ||
} | ||
|
||
private addPropGetter( | ||
obj: Record<string, unknown>, | ||
name: string, | ||
prop = name, | ||
) { | ||
Object.defineProperty(obj, name, { | ||
configurable: true, | ||
enumerable: true, | ||
get: () => this.componentInst[prop], | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { EventEmitter } from '@angular/core'; | ||
|
||
export interface IOData { | ||
[prop: string]: unknown; | ||
} | ||
|
||
type InferEventEmitter<T> = T extends EventEmitter<infer E> ? E : unknown; | ||
|
||
type SkipPropsByType<T, TSkip> = { | ||
[K in keyof T]: T[K] extends TSkip ? never : K; | ||
}[keyof T]; | ||
|
||
type PickPropsWithOutputs< | ||
O extends string | number | symbol, | ||
I extends string | number | symbol, | ||
> = O extends `${infer K}Change` ? (K extends I ? K : never) : never; | ||
|
||
export type Inputs<K extends keyof T, T> = Pick<T, K>; | ||
|
||
export type InputProps<K extends keyof T, T> = { | ||
[P in K as `[${P & string}]`]: T[P]; | ||
}; | ||
|
||
export type Inputs2Way<K> = { | ||
[P in K as `([${P & string}])`]: string; | ||
}; | ||
|
||
export type InputsAttrs = { | ||
[P in [] as `[attr.${string}]`]?: string | null; | ||
}; | ||
|
||
export type InputsClasses = { | ||
[P in [] as `[class.${string}]`]?: string | boolean | null; | ||
}; | ||
|
||
export type InputsStyles = { | ||
[P in [] as `[style.${string}]`]?: unknown; | ||
}; | ||
|
||
export type Outputs<K extends keyof T, T> = { | ||
[P in K as `(${P & string})`]: (event: InferEventEmitter<T[P]>) => void; | ||
}; | ||
|
||
export type IO< | ||
T, | ||
I extends keyof T = SkipPropsByType<T, EventEmitter<any>>, | ||
O extends keyof T = Exclude<keyof T, I>, | ||
I2W extends keyof T = PickPropsWithOutputs<O, I>, | ||
> = Partial< | ||
Inputs<I, T> & | ||
InputProps<I, T> & | ||
Inputs2Way<I2W> & | ||
Outputs<O, T> & | ||
InputsAttrs & | ||
InputsClasses & | ||
InputsStyles & | ||
Record<string, unknown> | ||
>; |
Oops, something went wrong.