diff --git a/packages/runtime/example/nested-components/index.html b/packages/runtime/example/nested-components/index.html index 4361f320..d52a2928 100644 --- a/packages/runtime/example/nested-components/index.html +++ b/packages/runtime/example/nested-components/index.html @@ -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", + }, + }, + }, + ], + }, ], }, }); diff --git a/packages/runtime/src/App.tsx b/packages/runtime/src/App.tsx index 8f79ee4d..dd9a72bc 100644 --- a/packages/runtime/src/App.tsx +++ b/packages/runtime/src/App.tsx @@ -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; +export type SlotsMap = Map>; +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; + } + ).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(() => ( + + )); + } 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 (
- {app.spec.components.map((c) => { - return ; + {topLevelComponents.map((c) => { + return ( + + ); })} diff --git a/packages/runtime/src/components/_internal/Slot.tsx b/packages/runtime/src/components/_internal/Slot.tsx new file mode 100644 index 00000000..ee27904c --- /dev/null +++ b/packages/runtime/src/components/_internal/Slot.tsx @@ -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) => ( + + ))} + + ); +}; + +export default Slot; diff --git a/packages/runtime/src/components/chakra-ui/Button.tsx b/packages/runtime/src/components/chakra-ui/Button.tsx index 864e9c45..61d294e1 100644 --- a/packages/runtime/src/components/chakra-ui/Button.tsx +++ b/packages/runtime/src/components/chakra-ui/Button.tsx @@ -11,7 +11,14 @@ const Button: ComponentImplementation<{ colorScheme?: Static; isLoading?: Static; 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 ( - + diff --git a/packages/runtime/src/components/chakra-ui/Tabs.tsx b/packages/runtime/src/components/chakra-ui/Tabs.tsx index 1b997f83..c9a3ecb0 100644 --- a/packages/runtime/src/components/chakra-ui/Tabs.tsx +++ b/packages/runtime/src/components/chakra-ui/Tabs.tsx @@ -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; initialSelectedTabIndex?: Static< typeof InitialSelectedTabIndexPropertySchema >; -}> = ({ tabNames, mergeState, initialSelectedTabIndex }) => { +}> = ({ tabNames, mergeState, initialSelectedTabIndex, slotsMap }) => { const [selectedTabIndex, setSelectedTabIndex] = useState( initialSelectedTabIndex ?? 0 ); @@ -37,9 +38,9 @@ const Tabs: ComponentImplementation<{ ))} - {tabNames.map((name, idx) => ( + {tabNames.map((_, idx) => ( -

{name}

+
))}
diff --git a/packages/runtime/src/registry.tsx b/packages/runtime/src/registry.tsx index 6c376a26..47435ed7 100644 --- a/packages/runtime/src/registry.tsx +++ b/packages/runtime/src/registry.tsx @@ -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 = 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); diff --git a/packages/runtime/src/store.ts b/packages/runtime/src/store.ts index 7ad214fe..be8bd431 100644 --- a/packages/runtime/src/store.ts +++ b/packages/runtime/src/store.ts @@ -7,9 +7,41 @@ export const useStore = create>(() => ({})); 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) { +export function evalInContext(raw: string, ctx: Record) { 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) { } } 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( - 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); } ); diff --git a/packages/runtime/src/traits/core/slot.ts b/packages/runtime/src/traits/core/slot.ts new file mode 100644 index 00000000..905667f8 --- /dev/null +++ b/packages/runtime/src/traits/core/slot.ts @@ -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: () => {}, +};