impl slot trait

To implement slot trait, we do the following changes
1. Nestable components should embed <Slot /> 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 <Slot />, so the embed <Slot /> can render which has been attached to itself.
This commit is contained in:
Yanzhen Yu 2021-07-26 16:30:36 +08:00
parent df84613296
commit 5850de1b33
6 changed files with 178 additions and 8 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

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

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