type safe test

This commit is contained in:
Yanzhen Yu 2021-12-21 19:35:47 +08:00
parent 9c5bc4a0ac
commit 376b529cd8
7 changed files with 197 additions and 66 deletions

View File

@ -22,6 +22,31 @@ type ComponentSpec = {
events: string[];
};
type ComponentSpec2<TMethodName extends string, K1, K2, K3> = Readonly<{
properties: JSONSchema7;
state: JSONSchema7;
methods: Record<TMethodName, MethodSchema['parameters']>;
styleSlots: ReadonlyArray<K1>;
slots: ReadonlyArray<K2>;
events: ReadonlyArray<K3>;
}>;
export type Component2<TMethodName extends string, K1, K2, K3> = {
version: string;
kind: 'Component';
metadata: ComponentMetadata;
spec: ComponentSpec2<TMethodName, K1, K2, K3>;
};
export type RuntimeComponentSpec2<TMethodName extends string, K1, K2, K3> = Component2<
TMethodName,
K1,
K2,
K3
> & {
parsedVersion: Version;
};
// extended runtime
export type RuntimeComponentSpec = Component & {
parsedVersion: Version;
@ -34,3 +59,18 @@ export function createComponent(options: Omit<Component, 'kind'>): RuntimeCompon
parsedVersion: parseVersion(options.version),
};
}
export type CreateComponentOptions2<TMethodName extends string, K1, K2, K3> = Omit<
Component2<TMethodName, K1, K2, K3>,
'kind'
>;
export function createComponent2<TMethodName extends string, K1, K2, K3>(
options: CreateComponentOptions2<TMethodName, K1, K2, K3>
): RuntimeComponentSpec2<TMethodName, K1, K2, K3> {
return {
...options,
kind: 'Component',
parsedVersion: parseVersion(options.version),
};
}

View File

@ -1,21 +1,33 @@
import React from 'react';
import { SlotsMap } from '../../types/RuntimeSchema';
export function getSlots<T>(slotsMap: SlotsMap | undefined, slot: string, rest: T) {
export function getSlots<T, K extends string>(
slotsMap: SlotsMap<K> | undefined,
slot: K,
rest: T
) {
return (slotsMap?.get(slot) || []).map(({ component: ImplWrapper, id }) => (
<ImplWrapper key={id} {...rest} />
));
}
const Slot: React.FC<{ slotsMap: SlotsMap | undefined; slot: string }> = ({
export type SlotType<K extends string> = React.FC<{
slotsMap: SlotsMap<K> | undefined;
slot: K;
}>;
function Slot<K extends string>({
slotsMap,
slot,
...rest
}) => {
}: {
slotsMap: SlotsMap<K> | undefined;
slot: K;
}): ReturnType<React.FC> {
if (!slotsMap?.has(slot)) {
return null;
}
return <>{getSlots(slotsMap, slot, rest)}</>;
};
}
export default Slot;

View File

@ -1,36 +1,52 @@
import { useEffect, useRef } from 'react';
import { createComponent } from '@sunmao-ui/core';
import {
createComponent2,
CreateComponentOptions2,
RuntimeComponentSpec2,
} from '@sunmao-ui/core';
import { Type, Static } from '@sinclair/typebox';
import Text, { TextPropertySchema } from '../_internal/Text';
import { ComponentImplementation } from '../../services/registry';
const Button: ComponentImplementation<Static<typeof PropsSchema>> = ({
text,
mergeState,
subscribeMethods,
callbackMap,
customStyle
}) => {
useEffect(() => {
mergeState({ value: text.raw });
}, [text.raw]);
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => {
subscribeMethods({
click() {
ref.current?.click();
},
});
}, []);
return (
<button ref={ref} onClick={callbackMap?.onClick} css={`${customStyle?.content}`}>
<Text value={text} />
</button>
);
export type ImplementedRuntimeComponent2<K1, K2, K3> = RuntimeComponentSpec2<
string,
K1,
K2,
K3
> & {
impl: ComponentImplementation;
};
function implementRuntimeComponent<
K extends string,
K1 extends string,
K2 extends string,
K3 extends string,
T extends CreateComponentOptions2<K, K1, K2, K3>
>(
options: T
): (
impl: ComponentImplementation<
Static<T['spec']['properties']>,
Static<T['spec']['state']>,
ToMap<T['spec']['methods']>,
ToStringUnion<T['spec']['slots']>,
ToStringUnion<T['spec']['styleSlots']>,
ToStringUnion<T['spec']['events']>
>
) => ImplementedRuntimeComponent2<K1, K2, K3> {
return impl => ({
...createComponent2(options),
impl,
});
}
type ToMap<U> = {
[K in keyof U]: Static<U[K]>;
};
type ToStringUnion<T extends ReadonlyArray<string>> = T[number];
const StateSchema = Type.Object({
value: Type.String(),
});
@ -39,8 +55,13 @@ const PropsSchema = Type.Object({
text: TextPropertySchema,
});
export default {
...createComponent({
export type ArrayItem<T extends readonly unknown[]> = T extends readonly (infer U)[]
? U
: never;
export default implementRuntimeComponent(
// T start
{
version: 'plain/v1',
metadata: {
name: 'button',
@ -58,16 +79,55 @@ export default {
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: [
{
name: 'click',
},
],
slots: [],
styleSlots: ['content'],
events: ['onClick'],
state: Type.Object({
value: Type.String(),
foo: Type.Boolean(),
}),
methods: {
click: Type.Object({
force: Type.Boolean(),
}),
dblClick: Type.Boolean(),
},
slots: ['prefix', 'suffix'],
styleSlots: ['wrapper', 'inner'],
events: ['onBlur'],
},
}),
impl: Button,
};
}
// T end
)(({ text, mergeState, subscribeMethods, callbackMap, customStyle, slotsMap, Slot }) => {
useEffect(() => {
// merget state
mergeState({
value: text.raw,
foo: false,
});
}, [text.raw]);
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => {
// subscribe methods
subscribeMethods({
click(params) {
console.log(params.force);
},
dblClick() {
//
},
});
}, []);
return (
<button
ref={ref}
css={`
${customStyle?.inner}
`}
onClick={callbackMap?.onBlur}
>
<Slot slotsMap={slotsMap} slot="prefix" />
<Text value={text} />
<Slot slotsMap={slotsMap} slot="suffix" />
</button>
);
});

View File

@ -6,6 +6,7 @@ import {
ImplWrapperProps,
TraitResult,
} from '../types/RuntimeSchema';
import Slot from 'src/components/_internal/Slot';
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
@ -156,6 +157,7 @@ export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
key={c.id}
{...mergedProps}
{...props}
Slot={Slot}
mergeState={mergeState}
subscribeMethods={subscribeMethods}
/>

View File

@ -56,7 +56,16 @@ import { parseType } from '../utils/parseType';
import { parseModuleSchema } from '../utils/parseModuleSchema';
import { cloneDeep } from 'lodash';
export type ComponentImplementation<T = any> = React.FC<T & ComponentImplementationProps>;
export type ComponentImplementation<
TProps = any,
TState = any,
TMethods = Record<string, unknown>,
TSlot extends string = string,
TStyleSlot extends string = string,
TEvent extends string = string
> = React.FC<
TProps & ComponentImplementationProps<TState, TMethods, TSlot, TStyleSlot, TEvent>
>;
export type ImplementedRuntimeComponent = RuntimeComponentSpec & {
impl: ComponentImplementation;
@ -136,7 +145,7 @@ export class Registry {
}
registerModule(c: ImplementedRuntimeModule, overWrite = false) {
const parsedModule = parseModuleSchema(cloneDeep(c))
const parsedModule = parseModuleSchema(cloneDeep(c));
if (!overWrite && this.modules.get(c.version)?.has(c.metadata.name)) {
throw new Error(
`Already has module ${c.version}/${c.metadata.name} in this registry.`

View File

@ -45,7 +45,7 @@ const useEventTrait: TraitImplementation<Static<typeof PropsSchema>> = ({
}
}
const callbackMap: CallbackMap = {};
const callbackMap: CallbackMap<string> = {};
for (const eventName in callbackQueueMap) {
callbackMap[eventName] = () => {

View File

@ -6,6 +6,7 @@ import { StateManager } from '../services/stateStore';
import { Application, RuntimeApplication } from '@sunmao-ui/core';
import { EventHandlerSchema } from './TraitPropertiesSchema';
import { Type } from '@sinclair/typebox';
import { SlotType } from '../components/_internal/Slot';
export type RuntimeApplicationComponent = RuntimeApplication['spec']['components'][0];
@ -40,44 +41,51 @@ export type AppProps = {
debugEvent?: boolean;
} & ComponentParamsFromApp;
export type ImplWrapperProps = {
export type ImplWrapperProps<TSlot extends string = string> = {
component: RuntimeApplicationComponent;
slotsMap: SlotsMap | undefined;
slotsMap: SlotsMap<TSlot> | undefined;
Slot: SlotType<TSlot>;
targetSlot: { id: string; slot: string } | null;
services: UIServices;
app?: RuntimeApplication;
} & ComponentParamsFromApp;
export type SlotComponentMap = Map<string, SlotsMap>;
export type SlotsMap = Map<
string,
export type SlotComponentMap = Map<string, SlotsMap<string>>;
export type SlotsMap<K extends string> = Map<
K,
Array<{
component: React.FC;
id: string;
}>
>;
export type CallbackMap = Record<string, () => void>;
export type CallbackMap<K extends string> = Record<K, () => void>;
export type SubscribeMethods = <U>(map: {
export type SubscribeMethods<U> = (map: {
[K in keyof U]: (parameters: U[K]) => void;
}) => void;
export type MergeState = (partialState: any) => void;
export type MergeState<T> = (partialState: T) => void;
type RuntimeFunctions = {
mergeState: MergeState;
subscribeMethods: SubscribeMethods;
type RuntimeFunctions<TState, TMethods> = {
mergeState: MergeState<TState>;
subscribeMethods: SubscribeMethods<TMethods>;
};
export type ComponentImplementationProps = ImplWrapperProps &
TraitResult['props'] &
RuntimeFunctions;
export type ComponentImplementationProps<
TState,
TMethods,
TSlot extends string,
TStyleSlot extends string,
TEvent extends string
> = ImplWrapperProps<TSlot> &
TraitResult<TStyleSlot, TEvent>['props'] &
RuntimeFunctions<TState, TMethods>;
export type TraitResult = {
export type TraitResult<TStyleSlot extends string, TEvent extends string> = {
props: {
data?: unknown;
customStyle?: Record<string, string>;
callbackMap?: CallbackMap;
customStyle?: Record<TStyleSlot, string>;
callbackMap?: CallbackMap<TEvent>;
effects?: Array<() => void>;
} | null;
unmount?: boolean;
@ -85,11 +93,11 @@ export type TraitResult = {
export type TraitImplementation<T = any> = (
props: T &
RuntimeFunctions & {
RuntimeFunctions<unknown, unknown> & {
componentId: string;
services: UIServices;
}
) => TraitResult;
) => TraitResult<string, string>;
export const RuntimeModuleSchema = Type.Object({
id: Type.String(),