feat: auto detect the raw properties types

This commit is contained in:
MrWindlike 2022-06-16 21:06:11 +08:00
parent 8a7028ce56
commit a25a86b48d
16 changed files with 131 additions and 87 deletions

View File

@ -309,12 +309,14 @@ export const Table = implementRuntimeComponent({
switch (evaledColumn.type) {
case 'button':
const handleClick = () => {
const rawColumn = (component.properties.columns as ColumnProperty[])[i];
if (!rawColumn.btnCfg) return;
const evaledButtonConfig = services.stateManager.deepEval(
rawColumn.btnCfg,
evalOptions
);
const rawColumns = component.properties.columns;
const evaledColumns =
typeof rawColumns === 'string'
? (services.stateManager.maskedEval(rawColumns) as ColumnProperty[])
: services.stateManager.deepEval(rawColumns, evalOptions);
const evaledButtonConfig = evaledColumns[i].btnCfg;
if (!evaledButtonConfig) return;
evaledButtonConfig.handlers.forEach(handler => {
services.apiService.send('uiMethod', {

View File

@ -32,14 +32,14 @@ export const FALLBACK_METADATA: ComponentMetadata = {
};
export const getComponentProps = <
T,
T extends Record<string, unknown>,
TState,
TMethods,
TSlots extends Record<string, SlotSpec>,
KStyleSlot extends string,
KEvent extends string
>(
props: T & ComponentImplProps<TState, TMethods, TSlots, KStyleSlot, KEvent>
props: T & ComponentImplProps<T, TState, TMethods, TSlots, KStyleSlot, KEvent>
) => {
const {
/* eslint-disable @typescript-eslint/no-unused-vars */

View File

@ -15,8 +15,6 @@ import { TablePagination } from './Pagination';
import { TableTd } from './TableTd';
import { implementTable } from './spec';
import { ColumnsPropertySpec } from './TableTypes';
import { Static } from '@sinclair/typebox';
type SortRule = {
key: string;
@ -187,20 +185,14 @@ export const TableImpl = implementTable(
}}
>
{isMultiSelect ? checkbox : undefined}
{columns.map((column, j) => (
{columns.map(column => (
<TableTd
index={i}
component={component}
key={column.key}
item={item}
onClickItem={() => selectItem(item)}
rawColumn={
(
component.properties.columns as Static<
typeof ColumnsPropertySpec
>
)[j]
}
rawColumns={component.properties.columns}
column={column}
services={services}
app={app}

View File

@ -11,18 +11,20 @@ import {
ExpressionError,
ImplWrapper,
} from '@sunmao-ui/runtime';
import { PropsBeforeEvaled } from '@sunmao-ui/shared';
export const TableTd: React.FC<{
index: number;
item: any;
column: Static<typeof ColumnSpec>;
rawColumn: Static<typeof ColumnsPropertySpec>[0];
rawColumns: string | PropsBeforeEvaled<Static<typeof ColumnsPropertySpec>>;
onClickItem: () => void;
services: UIServices;
component: RuntimeComponentSchema;
app: RuntimeApplication;
}> = props => {
const { item, index, component, column, rawColumn, onClickItem, services, app } = props;
const { item, index, component, column, rawColumns, onClickItem, services, app } =
props;
const evalOptions = {
evalListItem: true,
scopeObject: {
@ -61,8 +63,14 @@ export const TableTd: React.FC<{
case 'button':
const onClick = () => {
onClickItem();
rawColumn.buttonConfig.handlers.forEach(handler => {
const evaledHandler = services.stateManager.deepEval(handler, evalOptions);
const evaledColumns =
typeof rawColumns === 'string'
? (services.stateManager.maskedEval(rawColumns, evalOptions) as Static<
typeof ColumnsPropertySpec
>)
: services.stateManager.deepEval(rawColumns, evalOptions);
evaledColumns[index].buttonConfig.handlers.forEach(evaledHandler => {
services.apiService.send('uiMethod', {
componentId: evaledHandler.componentId,
name: evaledHandler.method.name,

View File

@ -35,6 +35,7 @@
"lodash": "^4.17.21"
},
"devDependencies": {
"@sunmao-ui/shared": "^0.1.0",
"@swc/core": "^1.2.121",
"@types/jest": "^26.0.23",
"@types/json-schema": "^7.0.7",

View File

@ -1,6 +1,6 @@
import { Metadata } from './metadata';
import { parseVersion, Version } from './version';
import { type PropsBeforeEvaled } from '@sunmao-ui/shared';
// spec
export type Application = {
@ -12,31 +12,35 @@ export type Application = {
};
};
export type ComponentSchema = {
export type ComponentSchema<CProperties = Record<string, unknown>> = {
id: string;
type: string;
// do runtime type check
properties: Record<string, unknown>;
properties: PropsBeforeEvaled<CProperties>;
traits: TraitSchema[];
// scopes TBD
};
export type TraitSchema = {
export type TraitSchema<TProperties = Record<string, unknown>> = {
type: string;
// do runtime type check
properties: Record<string, unknown>;
properties: PropsBeforeEvaled<TProperties>;
};
export type RuntimeTraitSchema = TraitSchema & {
parsedType: VersionAndName;
}
export type RuntimeTraitSchema<TProperties = Record<string, unknown>> =
TraitSchema<TProperties> & {
parsedType: VersionAndName;
};
type VersionAndName = {
version: string;
name: string;
};
export type RuntimeComponentSchema = Omit<ComponentSchema, 'traits'> & {
export type RuntimeComponentSchema<CProperties = Record<string, unknown>> = Omit<
ComponentSchema<CProperties>,
'traits'
> & {
parsedType: VersionAndName;
traits: RuntimeTraitSchema[];
};

View File

@ -7,6 +7,7 @@
"composite": true,
"rootDir": "src",
"module": "ESNext",
"moduleResolution": "Node",
"declaration": true,
"declarationDir": "lib"
},

View File

@ -11,7 +11,7 @@ import {
import { ImplWrapper } from './ImplWrapper';
import { watch } from '../../utils/watchReactivity';
import { ImplementedRuntimeModule, UIServices } from '../../types';
import { EventHandlerSpec, ModuleRenderSpec } from '@sunmao-ui/shared';
import { EventHandlerSpec, ModuleRenderSpec, PropsAfterEvaled } from '@sunmao-ui/shared';
import { resolveChildrenMap } from '../../utils/resolveChildrenMap';
import { initStateAndMethod } from '../../utils/initStateAndMethod';
import { ExpressionError } from '../../services/StateManager';
@ -42,9 +42,11 @@ const ModuleRendererContent = React.forwardRef<
scopeObject: evalScope,
}) as string | ExpressionError;
function evalObject<T extends Record<string, any>>(obj: T): T {
function evalObject<T extends Record<string, any> = Record<string, any>>(
obj: T
): PropsAfterEvaled<{ obj: T }>['obj'] {
const evalOptions = { evalListItem: true, scopeObject: evalScope };
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
return services.stateManager.mapValuesDeep(obj, ({ value }) => {
if (typeof value === 'string') {
return services.stateManager.maskedEval(value, evalOptions);
}

View File

@ -13,6 +13,7 @@ import {
consoleError,
ConsoleType,
ExpChunk,
PropsAfterEvaled,
} from '@sunmao-ui/shared';
dayjs.extend(relativeTime);
@ -142,7 +143,7 @@ export class StateManager {
path: Array<string | number>;
}) => void,
path: Array<string | number> = []
): T {
): PropsAfterEvaled<T> {
return mapValues(obj, (val, key: string | number) => {
return isArray(val)
? val.map((innerVal, idx) => {
@ -153,10 +154,13 @@ export class StateManager {
: isPlainObject(val)
? this.mapValuesDeep(val as unknown as T, fn, path.concat(key))
: fn({ value: val, key, obj, path: path.concat(key) });
}) as T;
}) as PropsAfterEvaled<T>;
}
deepEval<T extends Record<string, unknown>>(obj: T, options: EvalOptions = {}): T {
deepEval<T extends Record<string, unknown> | any[]>(
obj: T,
options: EvalOptions = {}
): PropsAfterEvaled<T> {
// just eval
const evaluated = this.mapValuesDeep(obj, ({ value }) => {
if (typeof value !== 'string') {
@ -168,9 +172,9 @@ export class StateManager {
return evaluated;
}
deepEvalAndWatch<T extends Record<string, unknown>>(
deepEvalAndWatch<T extends Record<string, unknown> | any[]>(
obj: T,
watcher: (params: { result: T }) => void,
watcher: (params: { result: PropsAfterEvaled<T> }) => void,
options: EvalOptions = {}
) {
const stops: ReturnType<typeof watch>[] = [];
@ -179,7 +183,7 @@ export class StateManager {
const evaluated = this.deepEval(obj, options);
// watch change
let resultCache: T = evaluated;
let resultCache: PropsAfterEvaled<T> = evaluated;
this.mapValuesDeep(obj, ({ value, path }) => {
const isDynamicExpression =
typeof value === 'string' &&

View File

@ -2,25 +2,42 @@ import { Static, Type } from '@sinclair/typebox';
import { debounce, throttle, delay } from 'lodash';
import { CallbackMap, UIServices } from '../../types';
import { implementRuntimeTrait } from '../../utils/buildKit';
import { EventHandlerSpec, CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
import {
EventHandlerSpec,
EventCallBackHandlerSpec,
CORE_VERSION,
CoreTraitName,
PropsBeforeEvaled,
} from '@sunmao-ui/shared';
const HandlersSpec = Type.Array(EventHandlerSpec);
const CallbackSpec = Type.Array(EventCallBackHandlerSpec);
export const EventTraitPropertiesSpec = Type.Object({
handlers: Type.Array(EventHandlerSpec),
handlers: HandlersSpec,
});
export const generateCallback = (
handler: Omit<Static<typeof EventHandlerSpec>, 'type'>,
rawHandler: Static<typeof EventHandlerSpec>,
handler: Omit<Static<typeof EventCallBackHandlerSpec>, 'type'>,
rawHandlers: string | PropsBeforeEvaled<Static<typeof CallbackSpec>>,
index: number,
services: UIServices,
slotProps?: unknown,
evalListItem?: boolean
) => {
const { stateManager } = services;
const send = () => {
// Eval before sending event to assure the handler object is evaled from the latest state.
const evaledHandler = services.stateManager.deepEval(rawHandler, {
const evalOptions = {
scopeObject: { $slot: slotProps },
evalListItem,
});
};
const evaledHandlers =
typeof rawHandlers === 'string'
? (stateManager.maskedEval(rawHandlers, evalOptions) as Static<
typeof EventCallBackHandlerSpec
>[])
: stateManager.deepEval(rawHandlers);
const evaledHandler = evaledHandlers[index];
if (evaledHandler.disabled && typeof evaledHandler.disabled === 'boolean') {
return;
@ -61,15 +78,23 @@ export default implementRuntimeTrait({
})(() => {
return ({ trait, handlers, services, slotProps, evalListItem }) => {
const callbackQueueMap: Record<string, Array<() => void>> = {};
const rawHandlers = trait.properties.handlers as Static<typeof EventHandlerSpec>[];
const rawHandlers = trait.properties.handlers;
// setup current handlers
for (const i in handlers) {
const handler = handlers[i];
if (!callbackQueueMap[handler.type]) {
callbackQueueMap[handler.type] = [];
}
callbackQueueMap[handler.type].push(
generateCallback(handler, rawHandlers[i], services, slotProps, evalListItem)
generateCallback(
handler,
rawHandlers,
Number(i),
services,
slotProps,
evalListItem
)
);
}

View File

@ -1,8 +1,7 @@
import { Static, Type } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import {
consoleWarn,
ConsoleType,
EventHandlerSpec,
EventCallBackHandlerSpec,
CoreTraitName,
CORE_VERSION,
@ -155,15 +154,10 @@ export default implementRuntimeTrait({
error: undefined,
},
});
const rawOnComplete = trait.properties.onComplete as Static<
typeof FetchTraitPropertiesSpec
>['onComplete'];
rawOnComplete?.forEach((rawHandler, index) => {
generateCallback(
onComplete![index],
rawHandler as Static<typeof EventHandlerSpec>,
services
)();
const rawOnComplete = trait.properties.onComplete;
onComplete?.forEach((_, index) => {
generateCallback(onComplete[index], rawOnComplete, index, services)();
});
} else {
// TODO: Add FetchError class and remove console info
@ -177,15 +171,10 @@ export default implementRuntimeTrait({
error,
},
});
const rawOnError = trait.properties.onError as Static<
typeof FetchTraitPropertiesSpec
>['onError'];
rawOnError?.forEach((rawHandler, index) => {
generateCallback(
onError![index],
rawHandler as Static<typeof EventHandlerSpec>,
services
)();
const rawOnError = trait.properties.onError;
onError?.forEach((_, index) => {
generateCallback(onError[index], rawOnError, index, services)();
});
}
},
@ -201,15 +190,10 @@ export default implementRuntimeTrait({
error: error.toString(),
},
});
const rawOnError = trait.properties.onError as Static<
typeof FetchTraitPropertiesSpec
>['onError'];
rawOnError?.forEach((rawHandler, index) => {
generateCallback(
onError![index],
rawHandler as Static<typeof EventHandlerSpec>,
services
)();
const rawOnError = trait.properties.onError;
onError?.forEach((_, index) => {
generateCallback(onError[index], rawOnError, index, services)();
});
}
);

View File

@ -10,8 +10,11 @@ import { UIServices, ComponentParamsFromApp } from './application';
import { TraitResult } from './trait';
// TODO: (type-safe), remove fallback type
export type ImplWrapperProps<KSlot extends string = string> = {
component: RuntimeComponentSchema;
export type ImplWrapperProps<
TProps extends Record<string, unknown> = Record<string, unknown>,
KSlot extends string = string
> = {
component: RuntimeComponentSchema<TProps>;
childrenMap: ChildrenMap<KSlot>;
services: UIServices;
isInModule: boolean;
@ -21,12 +24,13 @@ export type ImplWrapperProps<KSlot extends string = string> = {
} & ComponentParamsFromApp;
export type ComponentImplProps<
TProps extends Record<string, unknown>,
TState,
TMethods,
TSlots extends Record<string, SlotSpec>,
KStyleSlot extends string,
KEvent extends string
> = ImplWrapperProps &
> = ImplWrapperProps<TProps, string> &
TraitResult<KStyleSlot, KEvent>['props'] &
RuntimeFunctions<TState, TMethods, TSlots> & {
elementRef?: React.MutableRefObject<any>;
@ -34,13 +38,15 @@ export type ComponentImplProps<
};
export type ComponentImpl<
TProps = any,
TProps extends Record<string, unknown> = Record<string, unknown>,
TState = any,
TMethods = Record<string, any>,
TSlots extends Record<string, SlotSpec> = Record<string, any>,
KStyleSlot extends string = string,
KEvent extends string = string
> = React.FC<TProps & ComponentImplProps<TState, TMethods, TSlots, KStyleSlot, KEvent>>;
> = React.FC<
TProps & ComponentImplProps<TProps, TState, TMethods, TSlots, KStyleSlot, KEvent>
>;
export type ImplementedRuntimeComponent<
KMethodName extends string,
@ -48,7 +54,7 @@ export type ImplementedRuntimeComponent<
KSlot extends string,
KEvent extends string
> = RuntimeComponent<KMethodName, KStyleSlot, KSlot, KEvent> & {
impl: ComponentImpl;
impl: ComponentImpl<any>;
};
export type ChildrenMap<KSlot extends string> = Record<

View File

@ -14,10 +14,10 @@ export type TraitResult<KStyleSlot extends string, KEvent extends string> = {
unmount?: boolean;
};
export type TraitImpl<T = any> = (
props: T &
export type TraitImpl<TProperties = any> = (
props: TProperties &
RuntimeFunctions<unknown, unknown, any> & {
trait: RuntimeTraitSchema;
trait: RuntimeTraitSchema<TProperties>;
componentId: string;
services: UIServices;
evalListItem?: boolean;

View File

@ -33,7 +33,9 @@ export function implementRuntimeComponent<
options: T
): (
impl: ComponentImpl<
Static<T['spec']['properties']>,
Static<T['spec']['properties']> extends Record<string, unknown>
? Static<T['spec']['properties']>
: Record<string, unknown>,
Static<T['spec']['state']>,
ToMap<T['spec']['methods']>,
T['spec']['slots'],

View File

@ -1,2 +1,3 @@
export * from './spec';
export * from './condition';
export * from './utils';

View File

@ -0,0 +1,12 @@
// add the string type to every properties because it may be a expression
export type PropsBeforeEvaled<T> = T extends Record<string, any> | any[]
? { [K in keyof T]: PropsBeforeEvaled<T[K]> | string }
: T;
// remove the string type for every properties
export type PropsAfterEvaled<T> = T extends Record<string, any> | any[]
? {
[K in keyof T]: T[K] extends string | infer O
? PropsAfterEvaled<O>
: PropsAfterEvaled<T[K]>;
}
: T;