mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-27 08:39:59 +08:00
commit
1580d57b25
@ -26,6 +26,66 @@
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: "btn",
|
||||
type: "chakra_ui/v1/button",
|
||||
properties: {
|
||||
text: {
|
||||
raw: "in tab1",
|
||||
format: "plain",
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: "core/v1/slot",
|
||||
properties: {
|
||||
container: {
|
||||
id: "tabs",
|
||||
slot: "tab_content_0",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "text",
|
||||
type: "core/v1/text",
|
||||
properties: {
|
||||
value: {
|
||||
raw: "in tab2",
|
||||
format: "plain",
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: "core/v1/slot",
|
||||
properties: {
|
||||
container: {
|
||||
id: "tabs",
|
||||
slot: "tab_content_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "nested_tabs",
|
||||
type: "chakra_ui/v1/tabs",
|
||||
properties: {
|
||||
tabNames: ["Tab Three", "Tab Four"],
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: "core/v1/slot",
|
||||
properties: {
|
||||
container: {
|
||||
id: "tabs",
|
||||
slot: "tab_content_1",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
@ -1,4 +1,10 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Application,
|
||||
createApplication,
|
||||
@ -7,10 +13,13 @@ import {
|
||||
import { merge } from "lodash";
|
||||
import { registry } from "./registry";
|
||||
import { setStore, useStore, emitter } from "./store";
|
||||
import { ContainerPropertySchema } from "./traits/core/slot";
|
||||
import { Static } from "@sinclair/typebox";
|
||||
|
||||
const ImplWrapper: React.FC<{
|
||||
component: RuntimeApplication["spec"]["components"][0];
|
||||
}> = ({ component: c }) => {
|
||||
slotsMap: SlotsMap | undefined;
|
||||
}> = ({ component: c, slotsMap }) => {
|
||||
const Impl = registry.getComponent(
|
||||
c.parsedType.version,
|
||||
c.parsedType.name
|
||||
@ -68,6 +77,7 @@ const ImplWrapper: React.FC<{
|
||||
{...traitsProps}
|
||||
mergeState={mergeState}
|
||||
subscribeMethods={subscribeMethods}
|
||||
slotsMap={slotsMap}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -107,13 +117,63 @@ const DebugEvent: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export type ComponentsMap = Map<string, SlotsMap>;
|
||||
export type SlotsMap = Map<string, Array<React.FC>>;
|
||||
export function resolveNestedComponents(app: RuntimeApplication): {
|
||||
topLevelComponents: RuntimeApplication["spec"]["components"];
|
||||
componentsMap: ComponentsMap;
|
||||
} {
|
||||
const topLevelComponents: RuntimeApplication["spec"]["components"] = [];
|
||||
const componentsMap: ComponentsMap = new Map();
|
||||
|
||||
for (const c of app.spec.components) {
|
||||
const slotTrait = c.traits.find((t) => t.parsedType.name === "slot");
|
||||
if (slotTrait) {
|
||||
const { id, slot } = (
|
||||
slotTrait.properties as {
|
||||
container: Static<typeof ContainerPropertySchema>;
|
||||
}
|
||||
).container;
|
||||
if (!componentsMap.has(id)) {
|
||||
componentsMap.set(id, new Map());
|
||||
}
|
||||
if (!componentsMap.get(id)?.has(slot)) {
|
||||
componentsMap.get(id)?.set(slot, []);
|
||||
}
|
||||
componentsMap
|
||||
.get(id)
|
||||
?.get(slot)
|
||||
?.push(() => (
|
||||
<ImplWrapper component={c} slotsMap={componentsMap.get(c.id)} />
|
||||
));
|
||||
} else {
|
||||
topLevelComponents.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
topLevelComponents,
|
||||
componentsMap,
|
||||
};
|
||||
}
|
||||
|
||||
const App: React.FC<{ options: Application }> = ({ options }) => {
|
||||
const app = createApplication(options);
|
||||
const { topLevelComponents, componentsMap } = useMemo(
|
||||
() => resolveNestedComponents(app),
|
||||
[app]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
{app.spec.components.map((c) => {
|
||||
return <ImplWrapper key={c.id} component={c} />;
|
||||
{topLevelComponents.map((c) => {
|
||||
return (
|
||||
<ImplWrapper
|
||||
key={c.id}
|
||||
component={c}
|
||||
slotsMap={componentsMap.get(c.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<DebugStore />
|
||||
<DebugEvent />
|
||||
|
17
packages/runtime/src/components/_internal/Slot.tsx
Normal file
17
packages/runtime/src/components/_internal/Slot.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { SlotsMap } from "../../App";
|
||||
|
||||
const Slot: React.FC<{ slotsMap: SlotsMap | undefined; slot: string }> = ({
|
||||
slotsMap,
|
||||
slot,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{(slotsMap?.get(slot) || []).map((ImplWrapper, idx) => (
|
||||
<ImplWrapper key={idx} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slot;
|
@ -11,7 +11,14 @@ const Button: ComponentImplementation<{
|
||||
colorScheme?: Static<typeof ColorSchemePropertySchema>;
|
||||
isLoading?: Static<typeof IsLoadingPropertySchema>;
|
||||
onClick?: () => void;
|
||||
}> = ({ text, mergeState, subscribeMethods, onClick, ...rest }) => {
|
||||
}> = ({
|
||||
text,
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
onClick,
|
||||
colorScheme,
|
||||
isLoading,
|
||||
}) => {
|
||||
const raw = useExpression(text.raw);
|
||||
useEffect(() => {
|
||||
mergeState({ value: raw });
|
||||
@ -28,7 +35,7 @@ const Button: ComponentImplementation<{
|
||||
|
||||
return (
|
||||
<ChakraProvider>
|
||||
<BaseButton {...rest} ref={ref} onClick={onClick}>
|
||||
<BaseButton {...{ colorScheme, isLoading }} ref={ref} onClick={onClick}>
|
||||
<Text value={{ ...text, raw }} />
|
||||
</BaseButton>
|
||||
</ChakraProvider>
|
||||
|
@ -8,15 +8,16 @@ import {
|
||||
TabPanel,
|
||||
ChakraProvider,
|
||||
} from "@chakra-ui/react";
|
||||
import { ComponentImplementation } from "../../registry";
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
import { ComponentImplementation } from "../../registry";
|
||||
import Slot from "../_internal/Slot";
|
||||
|
||||
const Tabs: ComponentImplementation<{
|
||||
tabNames: Static<typeof TabNamesPropertySchema>;
|
||||
initialSelectedTabIndex?: Static<
|
||||
typeof InitialSelectedTabIndexPropertySchema
|
||||
>;
|
||||
}> = ({ tabNames, mergeState, initialSelectedTabIndex }) => {
|
||||
}> = ({ tabNames, mergeState, initialSelectedTabIndex, slotsMap }) => {
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(
|
||||
initialSelectedTabIndex ?? 0
|
||||
);
|
||||
@ -37,9 +38,9 @@ const Tabs: ComponentImplementation<{
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{tabNames.map((name, idx) => (
|
||||
{tabNames.map((_, idx) => (
|
||||
<TabPanel key={idx}>
|
||||
<p>{name}</p>
|
||||
<Slot slotsMap={slotsMap} slot={`tab_content_${idx}`} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { RuntimeComponent, RuntimeTrait } from "@meta-ui/core";
|
||||
import { setStore } from "./store";
|
||||
import { SlotsMap } from "./App";
|
||||
// components
|
||||
import PlainButton from "./components/plain/Button";
|
||||
import CoreText from "./components/core/Text";
|
||||
@ -9,6 +10,7 @@ import ChakraUITabs from "./components/chakra-ui/Tabs";
|
||||
// traits
|
||||
import CoreState from "./traits/core/state";
|
||||
import CoreEvent from "./traits/core/event";
|
||||
import CoreSlot from "./traits/core/slot";
|
||||
|
||||
type ImplementedRuntimeComponent = RuntimeComponent & {
|
||||
impl: ComponentImplementation;
|
||||
@ -29,6 +31,7 @@ export type ComponentImplementation<T = any> = React.FC<
|
||||
T & {
|
||||
mergeState: MergeState;
|
||||
subscribeMethods: SubscribeMethods;
|
||||
slotsMap: SlotsMap | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
@ -93,3 +96,4 @@ registry.registerComponent(ChakraUITabs);
|
||||
|
||||
registry.registerTrait(CoreState);
|
||||
registry.registerTrait(CoreEvent);
|
||||
registry.registerTrait(CoreSlot);
|
||||
|
@ -7,9 +7,41 @@ export const useStore = create<Record<string, any>>(() => ({}));
|
||||
|
||||
export const setStore = useStore.setState;
|
||||
|
||||
export function parseExpression(raw: string): {
|
||||
dynamic: boolean;
|
||||
expression: string;
|
||||
} {
|
||||
if (!raw) {
|
||||
return {
|
||||
dynamic: false,
|
||||
expression: raw,
|
||||
};
|
||||
}
|
||||
const matchArr = raw.match(/{{(.+)}}/);
|
||||
if (!matchArr) {
|
||||
return {
|
||||
dynamic: false,
|
||||
expression: raw,
|
||||
};
|
||||
}
|
||||
return {
|
||||
dynamic: true,
|
||||
expression: matchArr[1],
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: use web worker
|
||||
export function evalInContext(expression: string, ctx: Record<string, any>) {
|
||||
export function evalInContext(raw: string, ctx: Record<string, any>) {
|
||||
try {
|
||||
const { dynamic, expression } = parseExpression(raw);
|
||||
if (!dynamic) {
|
||||
try {
|
||||
// covert primitive types
|
||||
return eval(expression);
|
||||
} catch (error) {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
Object.keys(ctx).forEach((key) => {
|
||||
// @ts-ignore
|
||||
self[key] = ctx[key];
|
||||
@ -26,40 +58,16 @@ export function evalInContext(expression: string, ctx: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
export function useExpression(raw: string) {
|
||||
const { dynamic, expression } = useMemo(() => {
|
||||
if (!raw) {
|
||||
return {
|
||||
dynamic: false,
|
||||
expression: raw,
|
||||
};
|
||||
}
|
||||
const matchArr = raw.match(/{{(.+)}}/);
|
||||
if (!matchArr) {
|
||||
return {
|
||||
dynamic: false,
|
||||
expression: raw,
|
||||
};
|
||||
}
|
||||
return {
|
||||
dynamic: true,
|
||||
expression: matchArr[1],
|
||||
};
|
||||
}, [raw]);
|
||||
|
||||
const [state, setState] = useState<any>(
|
||||
evalInContext(expression, useStore.getState())
|
||||
evalInContext(raw, useStore.getState())
|
||||
);
|
||||
|
||||
if (!dynamic) {
|
||||
return state;
|
||||
}
|
||||
|
||||
useStore.subscribe(
|
||||
(value) => {
|
||||
setState(value);
|
||||
},
|
||||
(state) => {
|
||||
return evalInContext(expression, state);
|
||||
return evalInContext(raw, state);
|
||||
}
|
||||
);
|
||||
|
||||
|
28
packages/runtime/src/traits/core/slot.ts
Normal file
28
packages/runtime/src/traits/core/slot.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { createTrait } from "@meta-ui/core";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
export const ContainerPropertySchema = Type.Object({
|
||||
id: Type.String(),
|
||||
slot: Type.String(),
|
||||
});
|
||||
|
||||
export default {
|
||||
...createTrait({
|
||||
version: "core/v1",
|
||||
metadata: {
|
||||
name: "slot",
|
||||
description: "nested components by slots",
|
||||
},
|
||||
spec: {
|
||||
properties: [
|
||||
{
|
||||
name: "container",
|
||||
...ContainerPropertySchema,
|
||||
},
|
||||
],
|
||||
state: {},
|
||||
methods: [],
|
||||
},
|
||||
}),
|
||||
impl: () => {},
|
||||
};
|
Loading…
Reference in New Issue
Block a user