Merge pull request #17 from webzard-io/runtime

impl slot trait
This commit is contained in:
yz-yu 2021-07-26 16:43:05 +08:00 committed by GitHub
commit 1580d57b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 222 additions and 37 deletions

View File

@ -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",
},
},
},
],
},
],
},
});

View File

@ -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 />

View 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;

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

@ -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);
}
);

View 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: () => {},
};