From df8461329632cef8f16f12d8ade9ab49cb9baf0e Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Mon, 26 Jul 2021 16:30:16 +0800 Subject: [PATCH 1/2] refactor expression eval --- .../src/components/chakra-ui/Button.tsx | 11 +++- packages/runtime/src/store.ts | 62 +++++++++++-------- 2 files changed, 44 insertions(+), 29 deletions(-) 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/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); } ); From 5850de1b33fdfdf99ed8922e9bb782420a2993ba Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Mon, 26 Jul 2021 16:30:36 +0800 Subject: [PATCH 2/2] impl slot trait To implement slot trait, we do the following changes 1. Nestable components should embed inside itself. 2. Attachable components should use the slot trait to indicate which container they will be attached to. During app render, we will resolve the nested relation by traversing the components spec. This process will return top-level components and a resolved slots tree. Top-level components will be rendered directly. The resolved slots tree will be passed into every , so the embed can render which has been attached to itself. --- .../example/nested-components/index.html | 60 ++++++++++++++++ packages/runtime/src/App.tsx | 68 +++++++++++++++++-- .../runtime/src/components/_internal/Slot.tsx | 17 +++++ .../runtime/src/components/chakra-ui/Tabs.tsx | 9 +-- packages/runtime/src/registry.tsx | 4 ++ packages/runtime/src/traits/core/slot.ts | 28 ++++++++ 6 files changed, 178 insertions(+), 8 deletions(-) create mode 100644 packages/runtime/src/components/_internal/Slot.tsx create mode 100644 packages/runtime/src/traits/core/slot.ts 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/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/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: () => {}, +};