mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
type safe component: types and migrate one component
This commit is contained in:
parent
cf2efa658f
commit
c3834ae3ef
@ -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),
|
||||
};
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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}</>;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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);
|
||||
|
@ -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 '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(),
|
||||
|
46
packages/runtime/src/utils/buildKit.ts
Normal file
46
packages/runtime/src/utils/buildKit.ts
Normal 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,
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user