modify components to new slot

This commit is contained in:
Bowen Tan 2022-01-06 15:35:50 +08:00
parent dd974ea085
commit ca0a780d96
22 changed files with 192 additions and 191 deletions

View File

@ -292,7 +292,7 @@ export default implementRuntimeComponent({
styleSlots: ['content'],
events: [],
},
})(({ customStyle, Slot, ...restProps }) => {
})(({ customStyle, slotsElements, ...restProps }) => {
const styleProps = pick(restProps, StyleProps);
return (
<BaseBox
@ -307,7 +307,7 @@ export default implementRuntimeComponent({
${customStyle?.content}
`}
>
<Slot slot="content" />
{slotsElements.content}
</BaseBox>
);
});

View File

@ -37,7 +37,7 @@ export default implementRuntimeComponent({
styleSlots: [],
events: [],
},
})(({ size, defaultValue, isDisabled, Slot, mergeState }) => {
})(({ size, defaultValue, isDisabled, slotsElements, mergeState }) => {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
mergeState({ value });
@ -50,7 +50,7 @@ export default implementRuntimeComponent({
isDisabled={isDisabled}
onChange={val => setValue(val)}
>
<Slot slot="content" />
{slotsElements.content}
</BaseCheckboxGroup>
);
});

View File

@ -61,7 +61,7 @@ export default implementRuntimeComponent({
},
})(
({
Slot,
slotsElements,
subscribeMethods,
callbackMap: callbacks,
title: customerTitle,
@ -132,7 +132,7 @@ export default implementRuntimeComponent({
>
<AlertDialogHeader>{title}</AlertDialogHeader>
<AlertDialogBody>
<Slot slot="content" />
{slotsElements.content}
</AlertDialogBody>
<AlertDialogFooter>

View File

@ -42,8 +42,8 @@ export default implementRuntimeComponent({
callbackMap,
services,
customStyle,
Slot,
treeMap,
slotsElements,
childrenMap,
component
}) => {
const [invalidArray, setInvalidArray] = useState<boolean[]>([]);
@ -51,11 +51,11 @@ export default implementRuntimeComponent({
const formDataRef = useRef<Record<string, any>>({});
const formControlIds = useMemo<string[]>(() => {
return (
treeMap[component.id]?.content.map(slot => {
childrenMap[component.id]?.content.map(slot => {
return slot.id;
}) || []
);
}, [component.id, treeMap]);
}, [component.id, childrenMap]);
useEffect(() => {
setInvalidArray(
@ -149,7 +149,7 @@ export default implementRuntimeComponent({
${customStyle?.content}
`}
>
<Slot slot="content" />
{slotsElements.content}
{hideSubmit ? undefined : (
<Button
marginInlineStart="auto !important"

View File

@ -63,14 +63,14 @@ export default implementRuntimeComponent({
mergeState,
services,
customStyle,
Slot,
treeMap,
slotsElements,
childrenMap,
component,
}) => {
const [inputValue, setInputValue] = useState('');
// don't show Invalid state on component mount
const [hideInvalid, setHideInvalid] = useState(true);
const inputId = useMemo(() => first(treeMap[component.id].content)?.id || '', [component.id, treeMap]);
const inputId = useMemo(() => first(childrenMap[component.id]?.content)?.id || '', [component.id, childrenMap]);
const [validResult, setValidResult] = useState({
isInvalid: false,
errorMsg: '',
@ -129,7 +129,7 @@ export default implementRuntimeComponent({
}, [inputId, fieldName, isInvalid, isRequired, inputValue, mergeState]);
const placeholder = <Text color="gray.200">Please Add Input Here</Text>;
const slotView = <Slot {...FormItemCSS} slot="content" />;
const slotView = slotsElements.content;
return (
<FormControl

View File

@ -39,7 +39,7 @@ export default implementRuntimeComponent({
methods: {},
events: [],
},
})(({ direction, wrap, align, justify, spacing, contentChildren , customStyle }) => {
})(({ direction, wrap, align, justify, spacing, slotsElements, customStyle }) => {
return (
<BaseHStack
height="full"
@ -54,7 +54,7 @@ export default implementRuntimeComponent({
`}
{...{ direction, wrap, align, justify, spacing }}
>
{contentChildren}
{slotsElements.content}
</BaseHStack>
);
});

View File

@ -35,7 +35,7 @@ export default implementRuntimeComponent({
styleSlots: ['content'],
events: [],
},
})(({ defaultValue, isNumerical, Slot, mergeState, customStyle }) => {
})(({ defaultValue, isNumerical, slotsElements, mergeState, customStyle }) => {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
@ -54,7 +54,7 @@ export default implementRuntimeComponent({
${customStyle?.content}
`}
>
<Slot slot="content" />
{slotsElements.content}
</BaseRadioGroup>
);
});

View File

@ -21,7 +21,7 @@ export default implementRuntimeComponent({
styleSlots: [],
events: [],
},
})(({ Slot }) => {
})(({ slotsElements }) => {
return (
<ChakraProvider
theme={extendTheme({
@ -29,7 +29,7 @@ export default implementRuntimeComponent({
useSystemColorMode: false,
})}
>
<Slot slot="root" />
<>{slotsElements.root}</>
</ChakraProvider>
);
});

View File

@ -67,10 +67,10 @@ export default implementRuntimeComponent({
styleSlots: [],
events: [],
},
})(({ direction, wrap, align, justify, spacing, Slot }) => {
})(({ direction, wrap, align, justify, spacing, slotsElements }) => {
return (
<BaseStack {...{ direction, wrap, align, justify, spacing }}>
<Slot slot="content" />
{slotsElements.content}
</BaseStack>
);
});

View File

@ -9,7 +9,7 @@ import {
Text,
} from '@chakra-ui/react';
import { Type } from '@sinclair/typebox';
import { implementRuntimeComponent, genSlotsAsArray } from '@sunmao-ui/runtime';
import { implementRuntimeComponent } from '@sunmao-ui/runtime';
const StateSchema = Type.Object({
selectedTabIndex: Type.Number(),
@ -43,15 +43,15 @@ export default implementRuntimeComponent({
styleSlots: ['tabItem', 'tabContent'],
events: [],
},
})((props) => {
const { tabNames, mergeState, initialSelectedTabIndex, customStyle } = props
})(props => {
const { tabNames, mergeState, initialSelectedTabIndex, customStyle, slotsElements } =
props;
const [selectedTabIndex, setSelectedTabIndex] = useState(initialSelectedTabIndex ?? 0);
useEffect(() => {
mergeState({ selectedTabIndex });
}, [mergeState, selectedTabIndex]);
const slotComponents = genSlotsAsArray(props, 'content');
const placeholder = (
<Text color="gray">Slot content is empty.Please drag component to this slot.</Text>
);
@ -74,7 +74,7 @@ export default implementRuntimeComponent({
</TabList>
<TabPanels>
{tabNames.map((_, idx) => {
const Component = slotComponents[idx]
const ele = slotsElements.content ? slotsElements.content[idx] : placeholder;
return (
<TabPanel
key={idx}
@ -82,7 +82,7 @@ export default implementRuntimeComponent({
${customStyle?.tabContent}
`}
>
{Component ? <Component /> : placeholder}
{ele}
</TabPanel>
);
})}

View File

@ -64,7 +64,7 @@ export default implementRuntimeComponent({
hasArrow,
isDisabled,
defaultIsOpen,
Slot,
slotsElements,
}) => {
return (
/*
@ -80,7 +80,7 @@ export default implementRuntimeComponent({
defaultIsOpen={defaultIsOpen}
shouldWrapChildren={shouldWrapChildren}
>
<Slot slot="content" />
{slotsElements.content}
</Tooltip>
);
}

View File

@ -19,50 +19,42 @@ const PropsSchema = Type.Object({
});
export default implementRuntimeComponent({
version: 'chakra_ui/v1',
metadata: {
name: 'vstack',
displayName: 'VStack',
description: 'chakra-ui vstack',
exampleProperties: {
spacing: '24px',
},
exampleSize: [6, 6],
isDraggable: true,
isResizable: true,
version: 'chakra_ui/v1',
metadata: {
name: 'vstack',
displayName: 'VStack',
description: 'chakra-ui vstack',
exampleProperties: {
spacing: '24px',
},
spec: {
properties: PropsSchema,
state: Type.Object({}),
slots: ['content'],
styleSlots: ['content'],
methods: {},
events: [],
},
})(({
direction,
wrap,
align,
justify,
spacing,
contentChildren,
customStyle,
}) => {
return (
<BaseVStack
width="full"
height="full"
padding="4"
background="white"
border="1px solid"
borderColor="gray.200"
borderRadius="4"
className={css`
${customStyle?.content}
`}
{...{ direction, wrap, align, justify, spacing }}
>
{contentChildren}
</BaseVStack>
);
})
exampleSize: [6, 6],
isDraggable: true,
isResizable: true,
},
spec: {
properties: PropsSchema,
state: Type.Object({}),
slots: ['content'],
styleSlots: ['content'],
methods: {},
events: [],
},
})(({ direction, wrap, align, justify, spacing, slotsElements, customStyle }) => {
return (
<BaseVStack
width="full"
height="full"
padding="4"
background="white"
border="1px solid"
borderColor="gray.200"
borderRadius="4"
className={css`
${customStyle?.content}
`}
{...{ direction, wrap, align, justify, spacing }}
>
{slotsElements.content}
</BaseVStack>
);
});

View File

@ -1,5 +1,5 @@
import { createApplication } from '@sunmao-ui/core';
import { resolveTreeMap } from '../src/utils/resolveTreeMap';
import { resolveChildrenMap } from '../src/utils/resolveChildrenMap';
const origin = createApplication({
version: 'example/v1',
@ -82,24 +82,24 @@ const origin = createApplication({
});
describe('resolve tree map', () => {
const { treeMap, topLevelComponents } = resolveTreeMap(origin.spec.components);
const { childrenMap, topLevelComponents } = resolveChildrenMap(origin.spec.components);
it('resolve tree map', () => {
expect(treeMap['hstack1'].content.map(c => c.id)).toEqual(['button1', 'vstack1']);
expect(treeMap['vstack1'].content.map(c => c.id)).toEqual(['hstack2']);
expect(treeMap['hstack2'].content.map(c => c.id)).toEqual(['text1', 'text2']);
expect(childrenMap['hstack1'].content.map(c => c.id)).toEqual(['button1', 'vstack1']);
expect(childrenMap['vstack1'].content.map(c => c.id)).toEqual(['hstack2']);
expect(childrenMap['hstack2'].content.map(c => c.id)).toEqual(['text1', 'text2']);
expect(topLevelComponents.map(c => c.id)).toEqual(['hstack1', 'hstack3']);
expect(treeMap['hstack1']._grandChildren!.map(c => c.id)).toEqual([
expect(childrenMap['hstack1']._grandChildren!.map(c => c.id)).toEqual([
'button1',
'vstack1',
'hstack2',
'text1',
'text2',
]);
expect(treeMap['vstack1']._grandChildren!.map(c => c.id)).toEqual([
expect(childrenMap['vstack1']._grandChildren!.map(c => c.id)).toEqual([
'hstack2',
'text1',
'text2',
]);
expect(treeMap['hstack2']._grandChildren!.map(c => c.id)).toEqual(['text1', 'text2']);
expect(childrenMap['hstack2']._grandChildren!.map(c => c.id)).toEqual(['text1', 'text2']);
});
});

View File

@ -4,7 +4,7 @@ import { ImplWrapper } from './components/_internal/ImplWrapper';
import { AppProps, UIServices } from './types/RuntimeSchema';
import { DebugEvent, DebugStore } from './services/DebugComponents';
import { RuntimeAppSchemaManager } from './services/RuntimeAppSchemaManager';
import { resolveTreeMap } from './utils/resolveTreeMap';
import { resolveChildrenMap } from './utils/resolveChildrenMap';
// inject modules to App
export function genApp(services: UIServices) {
@ -26,7 +26,7 @@ export const App: React.FC<AppProps> = props => {
const app = runtimeAppSchemaManager.current.update(options);
initStateAndMethod(services.registry, services.stateManager, app.spec.components);
const { treeMap, topLevelComponents } = resolveTreeMap(app.spec.components);
const { childrenMap, topLevelComponents } = resolveChildrenMap(app.spec.components);
return (
<div className="App" style={{ height: '100vh', overflow: 'auto' }}>
{topLevelComponents.map(c => {
@ -35,7 +35,7 @@ export const App: React.FC<AppProps> = props => {
key={c.id}
component={c}
services={services}
treeMap={treeMap}
childrenMap={childrenMap}
app={app}
componentWrapper={componentWrapper}
gridCallbacks={gridCallbacks}

View File

@ -20,9 +20,8 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
children,
componentWrapper: ComponentWrapper,
services,
treeMap,
childrenMap,
} = props;
console.log('wrapper', c.id);
const { registry, stateManager, globalHandlerMap, apiService } = props.services;
const childrenCache = new Map<RuntimeApplicationComponent, React.ReactElement>();
@ -154,26 +153,30 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
}, [c.properties, stateManager]);
const mergedProps = { ...evaledComponentProperties, ...propsFromTraits };
const contentChildren = (
<>
{treeMap[c.id]
? treeMap[c.id]._allChildren.map(child => {
if (!childrenCache.get(child)) {
const ele = <ImplWrapper key={child.id} {...props} component={child} />;
console.log('render element', c.id);
childrenCache.set(child, ele);
}
return childrenCache.get(child);
})
: null}
</>
);
function genSlotsElements() {
if (!childrenMap[c.id]) {
return {};
}
const res: Record<string, React.ReactElement[]> = {};
for (const slot in childrenMap[c.id]) {
res[slot] = childrenMap[c.id][slot].map(child => {
if (!childrenCache.get(child)) {
const ele = <ImplWrapper key={child.id} {...props} component={child} />;
childrenCache.set(child, ele);
}
return childrenCache.get(child)!;
});
}
return res;
}
const C = unmount ? null : (
<Impl
key={c.id}
{...props}
{...mergedProps}
contentChildren={contentChildren}
slotsElements={genSlotsElements()}
mergeState={mergeState}
subscribeMethods={subscribeMethods}
/>
@ -227,8 +230,8 @@ const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props,
export const ImplWrapper = React.memo<ImplWrapperProps>(
_ImplWrapper,
(prevProps, nextProps) => {
const prevChildren = prevProps.treeMap[prevProps.component.id]?._grandChildren;
const nextChildren = nextProps.treeMap[nextProps.component.id]?._grandChildren;
const prevChildren = prevProps.childrenMap[prevProps.component.id]?._grandChildren;
const nextChildren = nextProps.childrenMap[nextProps.component.id]?._grandChildren;
if (!prevChildren || !nextProps) return false;
let isEqual = false;

View File

@ -12,7 +12,7 @@ import { EventHandlerSchema } from '../../types/TraitPropertiesSchema';
import { ImplWrapper } from './ImplWrapper';
import { watch } from '../../utils/watchReactivity';
import { ImplementedRuntimeModule } from '../../services/registry';
import { resolveTreeMap } from '../../utils/resolveTreeMap';
import { resolveChildrenMap } from '../../utils/resolveChildrenMap';
type Props = Static<typeof RuntimeModuleSchema> & {
evalScope?: Record<string, any>;
@ -148,7 +148,7 @@ const ModuleRendererContent: React.FC<Props & { moduleSpec: ImplementedRuntimeMo
}, [evaledHanlders, moduleId, services.apiService]);
const result = useMemo(() => {
const { treeMap, topLevelComponents } = resolveTreeMap(evaledModuleTemplate);
const { childrenMap, topLevelComponents } = resolveChildrenMap(evaledModuleTemplate);
return topLevelComponents.map(c => {
return (
<ImplWrapper
@ -156,7 +156,7 @@ const ModuleRendererContent: React.FC<Props & { moduleSpec: ImplementedRuntimeMo
component={c}
services={services}
app={app}
treeMap={treeMap}
childrenMap={childrenMap}
/>
);
});

View File

@ -40,7 +40,7 @@ export default implementRuntimeComponent({
styleSlots: ['content'],
events: [],
},
})(({ layout = [], gridCallbacks, component, customStyle, Slot }) => {
})(({ layout = [], gridCallbacks, component, customStyle, slotsElements }) => {
const onDragStop = gridCallbacks?.onDragStop
? partial(gridCallbacks.onDragStop, component.id)
: undefined;
@ -60,7 +60,7 @@ export default implementRuntimeComponent({
${customStyle?.content}
`}
>
<Slot slot='content' />
{slotsElements.content}
</BaseGridLayout>
</Suspense>
);

View File

@ -24,9 +24,8 @@ import {
import {
RuntimeApplicationComponent,
UIServices,
TreeMap,
ChildrenMap,
} from '../../../types/RuntimeSchema';
import { genSlotsAsArray } from '../../../components/_internal/Slot';
export type RouteLikeElement = PropsWithChildren<{
path?: string;
@ -65,14 +64,23 @@ type SwitchProps = {
location?: string;
switchPolicy: SwitchPolicy;
component: RuntimeApplicationComponent;
treeMap: TreeMap<string>;
childrenMap: ChildrenMap<string>;
services: UIServices;
slotsElements: Record<string, ReactElement[]>;
mergeState: (partialState: any) => void;
subscribeMethods: (map: { [key: string]: (parameters: any) => void }) => void;
};
export const Switch: React.FC<SwitchProps> = props => {
const { switchPolicy, location, mergeState, subscribeMethods } = props;
const {
switchPolicy,
location,
mergeState,
subscribeMethods,
slotsElements,
childrenMap,
component,
} = props;
const [originalLocation] = useLocation();
const matcher = useMemo(() => makeMatcher(), []);
@ -82,7 +90,7 @@ export const Switch: React.FC<SwitchProps> = props => {
let defaultPath: string | undefined = undefined;
const result = switchPolicy.map(
({ type, path, slotId, href, default: _default, exact, strict, sensitive }) => {
const componentsArr = genSlotsAsArray(props, slotId);
const children = slotsElements[slotId];
if (defaultPath === undefined && _default) {
defaultPath = path;
}
@ -101,19 +109,19 @@ export const Switch: React.FC<SwitchProps> = props => {
</Route>
);
case RouteType.ROUTE:
if (!componentsArr) {
if (!children) {
console.warn('component not registered to router');
return <></>;
}
if (componentsArr.length !== 1) {
if (children.length !== 1) {
console.warn('router slot can only have one component');
}
const C = componentsArr[0];
if (C.displayName === 'router') {
const ele = children[0];
if (childrenMap[component.id][slotId][0].parsedType.name === 'router') {
return (
// it should match both itself and its children path
<Nested path={`(${path}|${path}/.*)`} base={path} key={path}>
<C key={slotId}></C>
{ele}
</Nested>
);
}
@ -126,7 +134,7 @@ export const Switch: React.FC<SwitchProps> = props => {
path={path}
mergeState={mergeState}
>
<C key={slotId}></C>
{ele}
</Route>
);
default:
@ -143,7 +151,7 @@ export const Switch: React.FC<SwitchProps> = props => {
);
}
return result;
}, [mergeState, props, switchPolicy]);
}, [component.id, mergeState, slotsElements, switchPolicy, childrenMap]);
useEffect(() => {
subscribeMethods({

View File

@ -29,7 +29,6 @@ export * from './types/RuntimeSchema';
export * from './types/TraitPropertiesSchema';
export * from './constants';
export * from './services/registry';
export { genSlots, genSlotsAsArray } from './components/_internal/Slot';
export { ModuleRenderer } from './components/_internal/ModuleRenderer';
export { default as Text, TextPropertySchema } from './components/_internal/Text';

View File

@ -44,12 +44,12 @@ export type AppProps = {
// TODO: (type-safe), remove fallback type
export type ImplWrapperProps<KSlot extends string = string> = {
component: RuntimeApplicationComponent;
treeMap: TreeMap<KSlot>;
childrenMap: ChildrenMap<KSlot>;
services: UIServices;
app?: RuntimeApplication;
} & ComponentParamsFromApp;
export type TreeMap<KSlot extends string> = Record<
export type ChildrenMap<KSlot extends string> = Record<
string,
Record<KSlot, RuntimeApplicationComponent[]> & {
_grandChildren?: RuntimeApplicationComponent[];
@ -78,7 +78,7 @@ export type ComponentImplementationProps<
> = ImplWrapperProps<KSlot> &
TraitResult<KStyleSlot, KEvent>['props'] &
RuntimeFunctions<TState, TMethods> & {
contentChildren: Record<KSlot, React.ReactElement>
slotsElements: Record<KSlot, React.ReactElement[]>
};
export type TraitResult<KStyleSlot extends string, KEvent extends string> = {

View File

@ -0,0 +1,62 @@
import { RuntimeApplicationComponent, ChildrenMap } from '../types/RuntimeSchema';
export function resolveChildrenMap(components: RuntimeApplicationComponent[]): {
childrenMap: ChildrenMap<string>;
topLevelComponents: RuntimeApplicationComponent[];
} {
const childrenMap: ChildrenMap<string> = {};
const topLevelComponents: RuntimeApplicationComponent[] = [];
for (const c of components) {
const slotTrait = c.traits.find(t => t.parsedType.name === 'slot');
if (!slotTrait) {
topLevelComponents.push(c);
continue;
}
const { id, slot } = slotTrait.properties.container as any;
if (!childrenMap[id]) {
childrenMap[id] = {
_allChildren: [],
};
}
const children = childrenMap[id];
if (!children[slot]) {
children[slot] = [];
}
children[slot].push(c);
children._allChildren.push(c);
}
// get allChildren and grand children
function getAllChildren(id: string): RuntimeApplicationComponent[] {
if (!childrenMap[id]) {
return [];
}
if (childrenMap[id]?._grandChildren) {
return childrenMap[id]._grandChildren!;
}
const children = childrenMap[id];
childrenMap[id]._grandChildren = children._allChildren.reduce((res, curr) => {
const cccc = getAllChildren(curr.id);
return res.concat(cccc);
}, children._allChildren);
return childrenMap[id]._grandChildren!;
}
for (const id in childrenMap) {
childrenMap[id]._grandChildren = getAllChildren(id);
Object.defineProperty(childrenMap[id], '_allChildren', {
enumerable: false,
});
Object.defineProperty(childrenMap[id], '_grandChildren', {
enumerable: false,
});
}
return {
childrenMap,
topLevelComponents,
};
}

View File

@ -1,63 +0,0 @@
import { flatten } from 'lodash-es';
import { RuntimeApplicationComponent, TreeMap } from '../types/RuntimeSchema';
export function resolveTreeMap(components: RuntimeApplicationComponent[]): {
treeMap: TreeMap<string>;
topLevelComponents: RuntimeApplicationComponent[];
} {
const treeMap: TreeMap<string> = {};
const topLevelComponents: RuntimeApplicationComponent[] = [];
for (const c of components) {
const slotTrait = c.traits.find(t => t.parsedType.name === 'slot');
if (!slotTrait) {
topLevelComponents.push(c);
continue;
}
const { id, slot } = slotTrait.properties.container as any;
if (!treeMap[id]) {
treeMap[id] = {
_allChildren: []
};
}
const children = treeMap[id];
if (!children[slot]) {
children[slot] = [];
}
children[slot].push(c);
children._allChildren.push(c)
}
// get allChildren and grand children
function getAllChildren(id: string): RuntimeApplicationComponent[] {
if (!treeMap[id]) {
return [];
}
if (treeMap[id]?._grandChildren) {
return treeMap[id]._grandChildren!;
}
const children = treeMap[id];
treeMap[id]._grandChildren = children._allChildren.reduce((res, curr) => {
const cccc = getAllChildren(curr.id);
return res.concat(cccc);
}, children._allChildren);
return treeMap[id]._grandChildren!;
}
for (const id in treeMap) {
treeMap[id]._grandChildren = getAllChildren(id);
Object.defineProperty(treeMap[id], '_allChildren', {
enumerable: false,
});
Object.defineProperty(treeMap[id], '_grandChildren', {
enumerable: false,
});
}
return {
treeMap,
topLevelComponents,
};
}