Skip to content

Commit

Permalink
--wip-- [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
gund committed Mar 13, 2023
1 parent f2f4959 commit c03699a
Show file tree
Hide file tree
Showing 13 changed files with 1,005 additions and 33 deletions.
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;
}
}
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 {}
2 changes: 2 additions & 0 deletions projects/ng-dynamic-component/src/lib/dynamic-io-v2/index.ts
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';
2 changes: 2 additions & 0 deletions projects/ng-dynamic-component/src/lib/io/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from './types';
export * from './event-argument';
export * from './io.service';
export * from './io-factory.service';
export * from './io-data';
export * from './io-adapter.service';
184 changes: 184 additions & 0 deletions projects/ng-dynamic-component/src/lib/io/io-adapter.service.ts
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],
});
}
}
58 changes: 58 additions & 0 deletions projects/ng-dynamic-component/src/lib/io/io-data.ts
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>
>;
Loading

0 comments on commit c03699a

Please sign in to comment.