mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-27 08:39:59 +08:00
type safe test
This commit is contained in:
parent
9c5bc4a0ac
commit
376b529cd8
@ -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),
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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.`
|
||||
|
@ -45,7 +45,7 @@ const useEventTrait: TraitImplementation<Static<typeof PropsSchema>> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const callbackMap: CallbackMap = {};
|
||||
const callbackMap: CallbackMap<string> = {};
|
||||
|
||||
for (const eventName in callbackQueueMap) {
|
||||
callbackMap[eventName] = () => {
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user