type safe component: types and migrate one component

This commit is contained in:
Yanzhen Yu 2021-12-25 15:18:59 +08:00 committed by Bowen Tan
parent cf2efa658f
commit c3834ae3ef
11 changed files with 414 additions and 261 deletions

View File

@ -35,29 +35,86 @@ export type ComponentDefinition = {
};
export function createComponent(options: ComponentDefinition): RuntimeComponentSpec {
return (
{
version: options.version,
kind: ('Component' as any),
parsedVersion: parseVersion(options.version),
metadata: {
description: options.metadata.description || '',
isDraggable: true,
isResizable: true,
displayName: options.metadata.name,
exampleProperties: {},
exampleSize: [1, 1],
...options.metadata,
},
spec: {
properties: {},
state: {},
methods: [],
styleSlots: [],
slots: [],
events: [],
...options.spec,
},
}
);
return {
version: options.version,
kind: 'Component' as any,
parsedVersion: parseVersion(options.version),
metadata: {
description: options.metadata.description || '',
isDraggable: true,
isResizable: true,
displayName: options.metadata.name,
exampleProperties: {},
exampleSize: [1, 1],
...options.metadata,
},
spec: {
properties: {},
state: {},
methods: [],
styleSlots: [],
slots: [],
events: [],
...options.spec,
},
};
}
// TODO: (type-safe), rename version 2 to normal version
type ComponentSpec2<
KMethodName extends string,
KStyleSlot extends string,
KSlot extends string,
KEvent extends string
> = {
properties: JSONSchema7;
state: JSONSchema7;
methods: Record<KMethodName, MethodSchema['parameters']>;
styleSlots: ReadonlyArray<KStyleSlot>;
slots: ReadonlyArray<KSlot>;
events: ReadonlyArray<KEvent>;
};
export type Component2<
KMethodName extends string,
KStyleSlot extends string,
KSlot extends string,
KEvent extends string
> = {
version: string;
kind: 'Component';
metadata: ComponentMetadata;
spec: ComponentSpec2<KMethodName, KStyleSlot, KSlot, KEvent>;
};
export type RuntimeComponentSpec2<
KMethodName extends string,
KStyleSlot extends string,
KSlot extends string,
KEvent extends string
> = Component2<KMethodName, KStyleSlot, KSlot, KEvent> & {
parsedVersion: Version;
};
export type CreateComponentOptions2<
KMethodName extends string,
KStyleSlot extends string,
KSlot extends string,
KEvent extends string
> = Omit<Component2<KMethodName, KStyleSlot, KSlot, KEvent>, 'kind'>;
export function createComponent2<
KMethodName extends string,
KStyleSlot extends string,
KSlot extends string,
KEvent extends string
>(
options: CreateComponentOptions2<KMethodName, KStyleSlot, KSlot, KEvent>
): RuntimeComponentSpec2<KMethodName, KStyleSlot, KSlot, KEvent> {
return {
...options,
kind: 'Component',
parsedVersion: parseVersion(options.version),
};
}

View File

@ -5,6 +5,7 @@ import { ImplWrapper } from './services/ImplWrapper';
import { resolveAppComponents } from './services/resolveAppComponents';
import { AppProps, UIServices } from './types/RuntimeSchema';
import { DebugEvent, DebugStore } from './services/DebugComponents';
import { getSlotWithMap } from 'src/components/_internal/Slot';
// inject modules to App
export function genApp(services: UIServices) {
@ -38,14 +39,17 @@ export const App: React.FC<AppProps> = props => {
);
return (
<div className="App" style={{height: '100vh', overflow: 'auto'}}>
<div className="App" style={{ height: '100vh', overflow: 'auto' }}>
{topLevelComponents.map(c => {
const slotsMap = slotComponentsMap.get(c.id);
const Slot = getSlotWithMap(slotsMap);
return (
<ImplWrapper
key={c.id}
component={c}
services={services}
slotsMap={slotComponentsMap.get(c.id)}
slotsMap={slotsMap}
Slot={Slot}
targetSlot={null}
app={app}
componentWrapper={componentWrapper}

View File

@ -10,6 +10,7 @@ import { ImplWrapper } from '../../services/ImplWrapper';
import { watch } from '../../utils/watchReactivity';
import { parseTypeComponents } from '../../utils/parseType';
import { ImplementedRuntimeModule } from '../../services/registry';
import { getSlotWithMap } from './Slot';
type Props = Static<typeof RuntimeModuleSchema> & {
evalScope?: Record<string, any>;
@ -21,143 +22,153 @@ export const ModuleRenderer: React.FC<Props> = props => {
const { type, services } = props;
try {
const moduleSpec = services.registry.getModuleByType(type);
return <ModuleRendererContent {...props} moduleSpec={moduleSpec}/>
return <ModuleRendererContent {...props} moduleSpec={moduleSpec} />;
} catch {
return <span>Cannot find Module {type}.</span>
return <span>Cannot find Module {type}.</span>;
}
}
const ModuleRendererContent: React.FC<Props & {moduleSpec: ImplementedRuntimeModule}> = props => {
const { moduleSpec, properties, handlers, evalScope, services, app } = props;
const moduleId = services.stateManager.maskedEval(props.id, true, evalScope) as string;
function evalObject<T extends Record<string, any>>(obj: T): T {
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
if (typeof value === 'string') {
return services.stateManager.maskedEval(value, true, evalScope);
}
return value;
}).obj;
}
const evalWithScope = useCallback(<T extends Record<string, any>>(
obj: T,
scope: Record<string, any>
): T => {
const hasScopeKey = (exp: string) => {
return Object.keys(scope).some(key => exp.includes('{{') && exp.includes(key));
};
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
if (typeof value === 'string' && hasScopeKey(value)) {
return services.stateManager.maskedEval(value, true, scope);
}
return value;
}).obj;
}, [services.stateManager])
// first eval the property, handlers, id of module
const evaledProperties = evalObject(properties);
const evaledHanlders = evalObject(handlers);
const parsedtemplete = useMemo(
() => moduleSpec.impl.map(parseTypeComponents),
[moduleSpec]
);
// then eval the template and stateMap of module
const evaledStateMap = useMemo(() => {
// stateMap only use state i
return evalWithScope(moduleSpec.spec.stateMap, { $moduleId: moduleId });
}, [evalWithScope, moduleSpec.spec.stateMap, moduleId]);
const evaledModuleTemplate = useDeepCompareMemo(() => {
// here should only eval with evaledProperties, any other key not in evaledProperties should be ignored
// so we can asumme that template will not change if evaledProperties is the same
return evalWithScope(parsedtemplete, {
...evaledProperties,
$moduleId: moduleId,
});
}, [parsedtemplete, evaledProperties, moduleId]);
// listen component state change
useEffect(() => {
if (!evaledStateMap) return;
const stops: ReturnType<typeof watch>[] = [];
if (!services.stateManager.store[moduleId]) {
services.stateManager.store[moduleId] = {};
}
for (const stateKey in evaledStateMap) {
// init state
services.stateManager.store[moduleId][stateKey] = get(
services.stateManager.store,
evaledStateMap[stateKey]
);
// watch state
const stop = watch(
() => {
return get(services.stateManager.store, evaledStateMap[stateKey]);
},
newV => {
services.stateManager.store[moduleId] = {
...services.stateManager.store[moduleId],
[stateKey]: newV,
};
}
);
stops.push(stop);
}
return () => {
stops.forEach(s => s());
};
}, [evaledStateMap, moduleId, services]);
// listen module event
useEffect(() => {
if (!evaledHanlders) return;
const _handlers = evaledHanlders as Array<Static<typeof EventHandlerSchema>>;
const moduleEventHanlders: any[] = [];
_handlers.forEach(h => {
const moduleEventHanlder = ({ fromId, eventType }: Record<string, string>) => {
if (eventType === h.type && fromId === moduleId) {
services.apiService.send('uiMethod', {
componentId: h.componentId,
name: h.method.name,
parameters: h.method.parameters,
});
}
};
services.apiService.on('moduleEvent', moduleEventHanlder);
moduleEventHanlders.push(moduleEventHanlder);
});
return () => {
moduleEventHanlders.forEach(h => {
services.apiService.off('moduleEvent', h);
});
};
}, [evaledHanlders, moduleId, services.apiService]);
const result = useMemo(() => {
const { topLevelComponents, slotComponentsMap } = resolveAppComponents(
evaledModuleTemplate,
{
services,
app,
}
);
return topLevelComponents.map(c => (
<ImplWrapper
key={c.id}
component={c}
slotsMap={slotComponentsMap.get(c.id)}
targetSlot={null}
services={services}
app={app}
/>
));
}, [evaledModuleTemplate, services, app]);
return <>{result}</>;
};
const ModuleRendererContent: React.FC<Props & { moduleSpec: ImplementedRuntimeModule }> =
props => {
const { moduleSpec, properties, handlers, evalScope, services, app } = props;
const moduleId = services.stateManager.maskedEval(
props.id,
true,
evalScope
) as string;
function evalObject<T extends Record<string, any>>(obj: T): T {
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
if (typeof value === 'string') {
return services.stateManager.maskedEval(value, true, evalScope);
}
return value;
}).obj;
}
const evalWithScope = useCallback(<T extends Record<string, any>>(
obj: T,
scope: Record<string, any>
): T =>{
const hasScopeKey = (exp: string) => {
return Object.keys(scope).some(key => exp.includes('{{') && exp.includes(key));
};
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
if (typeof value === 'string' && hasScopeKey(value)) {
return services.stateManager.maskedEval(value, true, scope);
}
return value;
}).obj;
}, [services.stateManager]);
// first eval the property, handlers, id of module
const evaledProperties = evalObject(properties);
const evaledHanlders = evalObject(handlers);
const parsedtemplete = useMemo(
() => moduleSpec.impl.map(parseTypeComponents),
[moduleSpec]
);
// then eval the template and stateMap of module
const evaledStateMap = useMemo(() => {
// stateMap only use state i
return evalWithScope(moduleSpec.spec.stateMap, { $moduleId: moduleId });
}, [evalWithScope, moduleSpec.spec.stateMap, moduleId]);
const evaledModuleTemplate = useDeepCompareMemo(() => {
// here should only eval with evaledProperties, any other key not in evaledProperties should be ignored
// so we can asumme that template will not change if evaledProperties is the same
return evalWithScope(parsedtemplete, {
...evaledProperties,
$moduleId: moduleId,
});
}, [parsedtemplete, evaledProperties, moduleId]);
// listen component state change
useEffect(() => {
if (!evaledStateMap) return;
const stops: ReturnType<typeof watch>[] = [];
if (!services.stateManager.store[moduleId]) {
services.stateManager.store[moduleId] = {};
}
for (const stateKey in evaledStateMap) {
// init state
services.stateManager.store[moduleId][stateKey] = get(
services.stateManager.store,
evaledStateMap[stateKey]
);
// watch state
const stop = watch(
() => {
return get(services.stateManager.store, evaledStateMap[stateKey]);
},
newV => {
services.stateManager.store[moduleId] = {
...services.stateManager.store[moduleId],
[stateKey]: newV,
};
}
);
stops.push(stop);
}
return () => {
stops.forEach(s => s());
};
}, [evaledStateMap, moduleId, services]);
// listen module event
useEffect(() => {
if (!evaledHanlders) return;
const _handlers = evaledHanlders as Array<Static<typeof EventHandlerSchema>>;
const moduleEventHanlders: any[] = [];
_handlers.forEach(h => {
const moduleEventHanlder = ({ fromId, eventType }: Record<string, string>) => {
if (eventType === h.type && fromId === moduleId) {
services.apiService.send('uiMethod', {
componentId: h.componentId,
name: h.method.name,
parameters: h.method.parameters,
});
}
};
services.apiService.on('moduleEvent', moduleEventHanlder);
moduleEventHanlders.push(moduleEventHanlder);
});
return () => {
moduleEventHanlders.forEach(h => {
services.apiService.off('moduleEvent', h);
});
};
}, [evaledHanlders, moduleId, services.apiService]);
const result = useMemo(() => {
const { topLevelComponents, slotComponentsMap } = resolveAppComponents(
evaledModuleTemplate,
{
services,
app,
}
);
return topLevelComponents.map(c => {
const slotsMap = slotComponentsMap.get(c.id);
const Slot = getSlotWithMap(slotsMap);
return (
<ImplWrapper
key={c.id}
component={c}
slotsMap={slotsMap}
Slot={Slot}
targetSlot={null}
services={services}
app={app}
/>
);
});
}, [evaledModuleTemplate, services, app]);
return <>{result}</>;
};

View File

@ -1,26 +1,47 @@
import React from 'react';
import { SlotsMap } from '../../types/RuntimeSchema';
export function getSlots<T>(slotsMap: SlotsMap | undefined, slot: string, rest: T): React.ReactElement[] {
export type SlotType<K extends string> = React.FC<{
slot: K;
}>;
export function getSlots<T, K extends string>(
slotsMap: SlotsMap<K> | undefined,
slot: K,
rest: T
): React.ReactElement[] {
const components = slotsMap?.get(slot);
if (!components) {
const placeholder = <div key='slot-placeholder' style={{color: 'gray'}}>Slot {slot} is empty.Please drag component to this slot.</div>;
return [placeholder]
const placeholder = (
<div key="slot-placeholder" style={{ color: 'gray' }}>
Slot {slot} is empty.Please drag component to this slot.
</div>
);
return [placeholder];
}
return components.map(({ component: ImplWrapper, id }) => (
<ImplWrapper key={id} {...rest} />
));
}
const Slot: React.FC<{ slotsMap: SlotsMap | undefined; slot: string }> = ({
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 function getSlotWithMap<K extends string>(
slotsMap: SlotsMap<K> | undefined
): SlotType<K> {
return props => <Slot slotsMap={slotsMap} {...props} />;
}
export default Slot;

View File

@ -59,7 +59,7 @@ export const Route: React.FC<RouteProps> = ({ match, children, mergeState }) =>
type SwitchProps = {
location?: string;
switchPolicy: SwitchPolicy;
slotMap?: SlotsMap;
slotMap?: SlotsMap<string>;
mergeState: (partialState: any) => void;
subscribeMethods: (map: { [key: string]: (parameters: any) => void }) => void;
};
@ -241,11 +241,11 @@ const flattenChildren = (
}
return Array.isArray(children)
? ([] as ReactElement<RouteProps>[]).concat(
...children.map(c =>
c.type === Fragment
? flattenChildren(c.props.children as ReactElement<RouteProps>)
: flattenChildren(c)
...children.map(c =>
c.type === Fragment
? flattenChildren(c.props.children as ReactElement<RouteProps>)
: flattenChildren(c)
)
)
)
: [children];
};

View File

@ -1,16 +1,43 @@
import { useEffect, useRef } from 'react';
import { createComponent } from '@sunmao-ui/core';
import { Type, Static } from '@sinclair/typebox';
import { Type } from '@sinclair/typebox';
import Text, { TextPropertySchema } from '../_internal/Text';
import { ComponentImplementation } from '../../services/registry';
import { implementRuntimeComponent2 } from 'src/utils/buildKit';
const Button: ComponentImplementation<Static<typeof PropsSchema>> = ({
text,
mergeState,
subscribeMethods,
callbackMap,
customStyle
}) => {
const StateSchema = Type.Object({
value: Type.String(),
});
const PropsSchema = Type.Object({
text: TextPropertySchema,
});
export default implementRuntimeComponent2({
version: 'plain/v1',
metadata: {
name: 'button',
displayName: 'Button',
description: 'plain button',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'text',
format: 'plain',
},
},
exampleSize: [2, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: {
click: void 0,
},
slots: [],
styleSlots: ['content'],
events: ['onClick'],
},
})(({ text, mergeState, subscribeMethods, callbackMap, customStyle }) => {
useEffect(() => {
mergeState({ value: text.raw });
}, [mergeState, text.raw]);
@ -25,49 +52,12 @@ const Button: ComponentImplementation<Static<typeof PropsSchema>> = ({
}, [subscribeMethods]);
return (
<button ref={ref} onClick={callbackMap?.onClick} className={`${customStyle?.content}`}>
<button
ref={ref}
onClick={callbackMap?.onClick}
className={`${customStyle?.content}`}
>
<Text value={text} />
</button>
);
};
const StateSchema = Type.Object({
value: Type.String(),
});
const PropsSchema = Type.Object({
text: TextPropertySchema,
});
export default {
...createComponent({
version: 'plain/v1',
metadata: {
name: 'button',
displayName: 'Button',
description: 'plain button',
isDraggable: true,
isResizable: true,
exampleProperties: {
text: {
raw: 'text',
format: 'plain',
},
},
exampleSize: [2, 1],
},
spec: {
properties: PropsSchema,
state: StateSchema,
methods: [
{
name: 'click',
},
],
slots: [],
styleSlots: ['content'],
events: ['onClick'],
},
}),
impl: Button,
};

View File

@ -6,6 +6,7 @@ import {
ImplWrapperProps,
TraitResult,
} from '../types/RuntimeSchema';
import { getSlotWithMap } from 'src/components/_internal/Slot';
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
@ -79,7 +80,7 @@ export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
}, [c.id, mergeState, registry, services, subscribeMethods])
// result returned from traits
const [traitResults, setTraitResults] = useState<TraitResult[]>(() => {
const [traitResults, setTraitResults] = useState<TraitResult<string, string>[]>(() => {
return c.traits.map(trait =>
excecuteTrait(trait, stateManager.deepEval(trait.properties).result)
);
@ -112,9 +113,9 @@ export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
}, [c.traits, excecuteTrait, stateManager]);
// reduce traitResults
const propsFromTraits: TraitResult['props'] = useMemo(() => {
const propsFromTraits: TraitResult<string, string>['props'] = useMemo(() => {
return Array.from(traitResults.values()).reduce(
(prevProps, result: TraitResult) => {
(prevProps, result: TraitResult<string, string>) => {
if (!result.props) {
return prevProps;
}
@ -126,7 +127,7 @@ export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
return merge(prevProps, result.props, { effects });
},
{} as TraitResult['props']
{} as TraitResult<string, string>['props']
);
}, [traitResults]);
const unmount = traitResults.some(r => r.unmount);
@ -150,12 +151,15 @@ export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
}, [c.properties, stateManager]);
const mergedProps = { ...evaledComponentProperties, ...propsFromTraits };
const { slotsMap, ...restProps } = props;
const Slot = getSlotWithMap(slotsMap);
const C = unmount ? null : (
<Impl
key={c.id}
{...mergedProps}
{...props}
{...restProps}
Slot={Slot}
mergeState={mergeState}
subscribeMethods={subscribeMethods}
/>

View File

@ -6,7 +6,7 @@ import {
} from '@sunmao-ui/core';
// components
/* --- plain --- */
import PlainButton from '../components/plain/Button';
// import PlainButton from '../components/plain/Button';
import CoreText from '../components/core/Text';
import CoreGridLayout from '../components/core/GridLayout';
import CoreRouter from '../components/core/Router';
@ -30,7 +30,16 @@ import { parseType } from '../utils/parseType';
import { parseModuleSchema } from '../utils/parseModuleSchema';
import { cloneDeep } from 'lodash-es';
export type ComponentImplementation<T = any> = React.FC<T & ComponentImplementationProps>;
export type ComponentImplementation<
TProps = any,
TState = any,
TMethods = Record<string, any>,
KSlot extends string = string,
KStyleSlot extends string = string,
KEvent extends string = string
> = React.FC<
TProps & ComponentImplementationProps<TState, TMethods, KSlot, KStyleSlot, KEvent>
>;
export type ImplementedRuntimeComponent = RuntimeComponentSpec & {
impl: ComponentImplementation;
@ -51,9 +60,9 @@ export type SunmaoLib = {
};
export class Registry {
components = new Map<string, Map<string, ImplementedRuntimeComponent>>()
traits = new Map<string, Map<string, ImplementedRuntimeTrait>>()
modules = new Map<string, Map<string, ImplementedRuntimeModule>>()
components = new Map<string, Map<string, ImplementedRuntimeComponent>>();
traits = new Map<string, Map<string, ImplementedRuntimeTrait>>();
modules = new Map<string, Map<string, ImplementedRuntimeModule>>();
registerComponent(c: ImplementedRuntimeComponent) {
if (this.components.get(c.version)?.has(c.metadata.name)) {
@ -87,7 +96,7 @@ export class Registry {
res.push(component);
}
}
return res
return res;
}
registerTrait(t: ImplementedRuntimeTrait) {
@ -132,7 +141,7 @@ export class Registry {
res.push(trait);
}
}
return res
return res;
}
registerModule(c: ImplementedRuntimeModule, overWrite = false) {
@ -170,7 +179,8 @@ export class Registry {
export function initRegistry(): Registry {
const registry = new Registry();
registry.registerComponent(PlainButton);
// TODO: (type-safe) register v2 component
// registry.registerComponent(PlainButton);
registry.registerComponent(CoreText);
registry.registerComponent(CoreGridLayout);
registry.registerComponent(CoreRouter);

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 'src/components/_internal/Slot';
export type RuntimeApplicationComponent = RuntimeApplication['spec']['components'][0];
@ -40,44 +41,53 @@ export type AppProps = {
debugEvent?: boolean;
} & ComponentParamsFromApp;
export type ImplWrapperProps = {
// TODO: (type-safe), remove fallback type
export type ImplWrapperProps<KSlot extends string = string> = {
component: RuntimeApplicationComponent;
slotsMap: SlotsMap | undefined;
// TODO: (type-safe), remove slotsMap from props
slotsMap: SlotsMap<KSlot> | undefined;
Slot: SlotType<KSlot>;
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,
KSlot extends string,
KStyleSlot extends string,
KEvent extends string
> = ImplWrapperProps<KSlot> &
TraitResult<KStyleSlot, KEvent>['props'] &
RuntimeFunctions<TState, TMethods>;
export type TraitResult = {
export type TraitResult<KStyleSlot extends string, KEvent extends string> = {
props: {
data?: unknown;
customStyle?: Record<string, string>;
callbackMap?: CallbackMap;
customStyle?: Record<KStyleSlot, string>;
callbackMap?: CallbackMap<KEvent>;
effects?: Array<() => void>;
} | null;
unmount?: boolean;
@ -85,11 +95,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(),

View File

@ -0,0 +1,46 @@
import { Static } from '@sinclair/typebox';
import {
createComponent2,
CreateComponentOptions2,
RuntimeComponentSpec2,
} from '@sunmao-ui/core';
import { ComponentImplementation } from 'src/services/registry';
export type ImplementedRuntimeComponent2<
KMethodName extends string,
KStyleSlot extends string,
KSlot extends string,
KEvent extends string
> = RuntimeComponentSpec2<KMethodName, KStyleSlot, KSlot, KEvent> & {
impl: ComponentImplementation;
};
type ToMap<U> = {
[K in keyof U]: Static<U[K]>;
};
type ToStringUnion<T extends ReadonlyArray<string>> = T[number];
export function implementRuntimeComponent2<
KMethodName extends string,
KStyleSlot extends string,
KSlot extends string,
KEvent extends string,
T extends CreateComponentOptions2<KMethodName, KStyleSlot, KSlot, KEvent>
>(
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<KMethodName, KStyleSlot, KSlot, KEvent> {
return impl => ({
...createComponent2(options),
impl,
});
}