mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-06 21:40:23 +08:00
Merge pull request #203 from webzard-io/refactor/slot
refactor slot and ImplWrapper to cache render result
This commit is contained in:
commit
432314c61e
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { Button, VStack } from '@chakra-ui/react';
|
||||
import { implementRuntimeComponent, Slot, watch } from '@sunmao-ui/runtime';
|
||||
import { implementRuntimeComponent, watch } from '@sunmao-ui/runtime';
|
||||
|
||||
const PropsSchema = Type.Object({
|
||||
hideSubmit: Type.Boolean(),
|
||||
@ -39,21 +39,23 @@ export default implementRuntimeComponent({
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
hideSubmit,
|
||||
slotsMap,
|
||||
callbackMap,
|
||||
services,
|
||||
customStyle,
|
||||
slotsElements,
|
||||
childrenMap,
|
||||
component
|
||||
}) => {
|
||||
const [invalidArray, setInvalidArray] = useState<boolean[]>([]);
|
||||
const [isFormInvalid, setIsFormInvalid] = useState<boolean>(false);
|
||||
const formDataRef = useRef<Record<string, any>>({});
|
||||
const formControlIds = useMemo<string[]>(() => {
|
||||
return (
|
||||
slotsMap?.get('content')?.map(slot => {
|
||||
childrenMap[component.id]?.content.map(slot => {
|
||||
return slot.id;
|
||||
}) || []
|
||||
);
|
||||
}, [slotsMap]);
|
||||
}, [component.id, childrenMap]);
|
||||
|
||||
useEffect(() => {
|
||||
setInvalidArray(
|
||||
@ -147,7 +149,7 @@ export default implementRuntimeComponent({
|
||||
${customStyle?.content}
|
||||
`}
|
||||
>
|
||||
<Slot slotsMap={slotsMap} slot="content" />
|
||||
{slotsElements.content}
|
||||
{hideSubmit ? undefined : (
|
||||
<Button
|
||||
marginInlineStart="auto !important"
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { css } from '@emotion/css';
|
||||
import { implementRuntimeComponent, Slot, watch } from '@sunmao-ui/runtime';
|
||||
import { implementRuntimeComponent, watch } from '@sunmao-ui/runtime';
|
||||
import { CheckboxStateSchema } from '../Checkbox';
|
||||
|
||||
const FormItemCSS = {
|
||||
@ -60,15 +60,17 @@ export default implementRuntimeComponent({
|
||||
fieldName,
|
||||
isRequired,
|
||||
helperText,
|
||||
slotsMap,
|
||||
mergeState,
|
||||
services,
|
||||
customStyle,
|
||||
slotsElements,
|
||||
childrenMap,
|
||||
component,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
// don't show Invalid state on component mount
|
||||
const [hideInvalid, setHideInvalid] = useState(true);
|
||||
const inputId = useMemo(() => first(slotsMap?.get('content'))?.id || '', [slotsMap]);
|
||||
const inputId = useMemo(() => first(childrenMap[component.id]?.content)?.id || '', [component.id, childrenMap]);
|
||||
const [validResult, setValidResult] = useState({
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
@ -127,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} slotsMap={slotsMap} slot="content" />;
|
||||
const slotView = slotsElements.content;
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
|
@ -39,7 +39,7 @@ export default implementRuntimeComponent({
|
||||
methods: {},
|
||||
events: [],
|
||||
},
|
||||
})(({ direction, wrap, align, justify, spacing, Slot, customStyle }) => {
|
||||
})(({ direction, wrap, align, justify, spacing, slotsElements, customStyle }) => {
|
||||
return (
|
||||
<BaseHStack
|
||||
height="full"
|
||||
@ -54,7 +54,7 @@ export default implementRuntimeComponent({
|
||||
`}
|
||||
{...{ direction, wrap, align, justify, spacing }}
|
||||
>
|
||||
<Slot slot="content" />
|
||||
{slotsElements.content}
|
||||
</BaseHStack>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { implementRuntimeComponent, getSlots } from '@sunmao-ui/runtime';
|
||||
import { implementRuntimeComponent } from '@sunmao-ui/runtime';
|
||||
|
||||
const StateSchema = Type.Object({
|
||||
selectedTabIndex: Type.Number(),
|
||||
@ -43,14 +43,15 @@ export default implementRuntimeComponent({
|
||||
styleSlots: ['tabItem', 'tabContent'],
|
||||
events: [],
|
||||
},
|
||||
})(({ tabNames, mergeState, initialSelectedTabIndex, slotsMap, customStyle }) => {
|
||||
})(props => {
|
||||
const { tabNames, mergeState, initialSelectedTabIndex, customStyle, slotsElements } =
|
||||
props;
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(initialSelectedTabIndex ?? 0);
|
||||
|
||||
useEffect(() => {
|
||||
mergeState({ selectedTabIndex });
|
||||
}, [mergeState, selectedTabIndex]);
|
||||
|
||||
const slotComponents = getSlots(slotsMap, 'content', {});
|
||||
const placeholder = (
|
||||
<Text color="gray">Slot content is empty.Please drag component to this slot.</Text>
|
||||
);
|
||||
@ -73,6 +74,7 @@ export default implementRuntimeComponent({
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{tabNames.map((_, idx) => {
|
||||
const ele = slotsElements.content ? slotsElements.content[idx] : placeholder;
|
||||
return (
|
||||
<TabPanel
|
||||
key={idx}
|
||||
@ -80,7 +82,7 @@ export default implementRuntimeComponent({
|
||||
${customStyle?.tabContent}
|
||||
`}
|
||||
>
|
||||
{slotComponents[idx] || placeholder}
|
||||
{ele}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
Slot,
|
||||
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 }}
|
||||
>
|
||||
<Slot slot="content" />
|
||||
</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>
|
||||
);
|
||||
});
|
||||
|
@ -55,7 +55,7 @@ const TYPE_REG = /^([a-zA-Z0-9_\d]+\/[a-zA-Z0-9_\d]+)\/([a-zA-Z0-9_\d]+)$/;
|
||||
function isValidType(v: string): boolean {
|
||||
return TYPE_REG.test(v);
|
||||
}
|
||||
function parseType(v: string): VersionAndName {
|
||||
export function parseType(v: string): VersionAndName {
|
||||
if (!isValidType(v)) {
|
||||
throw new Error(`Invalid type string: "${v}"`);
|
||||
}
|
||||
@ -67,7 +67,7 @@ function parseType(v: string): VersionAndName {
|
||||
};
|
||||
}
|
||||
|
||||
function isValidId(id: string): boolean {
|
||||
export function isValidId(id: string): boolean {
|
||||
return /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(id);
|
||||
}
|
||||
|
||||
|
@ -4,3 +4,4 @@ export * from './scope';
|
||||
export * from './application';
|
||||
export * from './method';
|
||||
export * from './module';
|
||||
export * from './version';
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { parseType } from '@sunmao-ui/runtime';
|
||||
import { ApplicationComponent, parseType } from '@sunmao-ui/core';
|
||||
import { ComponentModel } from './ComponentModel';
|
||||
import {
|
||||
ComponentId,
|
||||
|
@ -2,7 +2,8 @@ import React from 'react';
|
||||
import { flatten } from 'lodash-es';
|
||||
import { FormControl, FormLabel, Input, Textarea, VStack } from '@chakra-ui/react';
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
import { parseType, parseTypeBox } from '@sunmao-ui/runtime';
|
||||
import { parseType } from '@sunmao-ui/core';
|
||||
import { parseTypeBox } from '@sunmao-ui/runtime';
|
||||
import { eventBus } from '../../eventBus';
|
||||
import { EventTraitForm } from './EventTraitForm';
|
||||
import { GeneralTraitFormList } from './GeneralTraitFormList';
|
||||
|
@ -26,10 +26,10 @@ export const ComponentTree: React.FC<Props> = props => {
|
||||
if (slots.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const slotsMap = childrenMap.get(component.id);
|
||||
const children = childrenMap.get(component.id);
|
||||
return slots.map(slot => {
|
||||
let slotContent;
|
||||
const slotChildren = slotsMap?.get(slot);
|
||||
const slotChildren = children?.get(slot);
|
||||
if (slotChildren && slotChildren.length > 0) {
|
||||
slotContent = slotChildren.map(c => {
|
||||
return (
|
||||
|
@ -29,8 +29,10 @@ export const EmptyAppSchema: Application = {
|
||||
{
|
||||
type: 'core/v1/style',
|
||||
properties: {
|
||||
styleSlot: 'content',
|
||||
style: "background: red",
|
||||
styles: [{
|
||||
styleSlot: 'content',
|
||||
style: 'background: red',
|
||||
}],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { parseType } from '@sunmao-ui/runtime';
|
||||
import { ApplicationComponent, parseType } from '@sunmao-ui/core';
|
||||
import { isDraft, original } from 'immer';
|
||||
import { get } from 'lodash-es';
|
||||
import { registry } from '../setup';
|
||||
@ -36,12 +35,15 @@ export function getComponentAndChildrens(
|
||||
if (!target) {
|
||||
return [];
|
||||
}
|
||||
return allComponents.reduce<ApplicationComponent[]>((result, component) => {
|
||||
const slotTrait = component.traits.find(trait => trait.type === 'core/v1/slot');
|
||||
const slotId = get(slotTrait, 'properties.container.id');
|
||||
if (slotId === componentId) {
|
||||
return result.concat(getComponentAndChildrens(component.id, allComponents));
|
||||
}
|
||||
return result;
|
||||
}, [target]);
|
||||
return allComponents.reduce<ApplicationComponent[]>(
|
||||
(result, component) => {
|
||||
const slotTrait = component.traits.find(trait => trait.type === 'core/v1/slot');
|
||||
const slotId = get(slotTrait, 'properties.container.id');
|
||||
if (slotId === componentId) {
|
||||
return result.concat(getComponentAndChildrens(component.id, allComponents));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[target]
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
export type ChildrenMap = Map<string, SlotsMap>;
|
||||
type SlotsMap = Map<string, ApplicationComponent[]>;
|
||||
|
||||
// similar to resolveAppComponents
|
||||
export function resolveApplicationComponents(components: ApplicationComponent[]): {
|
||||
topLevelComponents: ApplicationComponent[];
|
||||
childrenMap: ChildrenMap;
|
||||
|
105
packages/runtime/__tests__/resolveChildrenMap.spec.ts
Normal file
105
packages/runtime/__tests__/resolveChildrenMap.spec.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { createApplication } from '@sunmao-ui/core';
|
||||
import { resolveChildrenMap } from '../src/utils/resolveChildrenMap';
|
||||
|
||||
const origin = createApplication({
|
||||
version: 'example/v1',
|
||||
metadata: { name: 'dialog_component', description: 'dialog component example' },
|
||||
spec: {
|
||||
components: [
|
||||
{
|
||||
id: 'hstack1',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'button1',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: { raw: 'text', format: 'plain' },
|
||||
colorScheme: 'blue',
|
||||
isLoading: false,
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'hstack1', slot: 'content' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'vstack1',
|
||||
type: 'chakra_ui/v1/vstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'hstack1', slot: 'content' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'hstack2',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'vstack1', slot: 'content' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'text1',
|
||||
type: 'core/v1/text',
|
||||
properties: { value: { raw: 'text', format: 'plain' } },
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'hstack2', slot: 'content' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'text2',
|
||||
type: 'core/v1/text',
|
||||
properties: { value: { raw: 'text', format: 'plain' } },
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'hstack2', slot: 'content' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'hstack3',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
describe('resolve tree map', () => {
|
||||
const { childrenMap, topLevelComponents } = resolveChildrenMap(origin.spec.components);
|
||||
it('resolve tree map', () => {
|
||||
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(childrenMap['hstack1']._grandChildren!.map(c => c.id)).toEqual([
|
||||
'button1',
|
||||
'vstack1',
|
||||
'hstack2',
|
||||
'text1',
|
||||
'text2',
|
||||
]);
|
||||
expect(childrenMap['vstack1']._grandChildren!.map(c => c.id)).toEqual([
|
||||
'hstack2',
|
||||
'text1',
|
||||
'text2',
|
||||
]);
|
||||
expect(childrenMap['hstack2']._grandChildren!.map(c => c.id)).toEqual(['text1', 'text2']);
|
||||
});
|
||||
});
|
86
packages/runtime/__tests__/runtimeAppSchemaManager.spec.ts
Normal file
86
packages/runtime/__tests__/runtimeAppSchemaManager.spec.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Application } from '@sunmao-ui/core';
|
||||
import { RuntimeAppSchemaManager } from '../src/services/RuntimeAppSchemaManager';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
const origin: Application = {
|
||||
kind: 'Application',
|
||||
version: 'example/v1',
|
||||
metadata: { name: 'dialog_component', description: 'dialog component example' },
|
||||
spec: {
|
||||
components: [
|
||||
{
|
||||
id: 'hstack1',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: { spacing: '24px' },
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'button1',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: { raw: 'text', format: 'plain' },
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'hstack1', slot: 'content' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'button2',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: { raw: 'text', format: 'plain' },
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: 'hstack1', slot: 'content' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'button3',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: { raw: 'text', format: 'plain' },
|
||||
colorScheme: 'blue',
|
||||
isLoading: false,
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const update: Application = cloneDeep(origin);
|
||||
update.spec.components = [...origin.spec.components];
|
||||
update.spec.components[2] = cloneDeep(update.spec.components[2]);
|
||||
(update.spec.components[2].properties.text as any).raw = '666';
|
||||
|
||||
describe('runtime update app schema', () => {
|
||||
const manager = new RuntimeAppSchemaManager();
|
||||
const s1 = manager.update(origin);
|
||||
const s2 = manager.update(update);
|
||||
it('keep immutable', () => {
|
||||
expect(s1).not.toBe(s2);
|
||||
expect(s1.spec.components[0]).toBe(s2.spec.components[0]);
|
||||
expect(s1.spec.components[0].parsedType).toEqual({
|
||||
name: 'hstack',
|
||||
version: 'chakra_ui/v1',
|
||||
});
|
||||
expect(s1.spec.components[1]).toBe(s2.spec.components[1]);
|
||||
expect(s1.spec.components[3]).toBe(s2.spec.components[3]);
|
||||
expect(origin.spec.components[2]).not.toBe(update.spec.components[2]);
|
||||
expect(s1.spec.components[2]).not.toBe(s2.spec.components[2]);
|
||||
expect(s2.spec.components[2].properties.text).toEqual({
|
||||
raw: '666',
|
||||
format: 'plain',
|
||||
});
|
||||
expect(s1.spec.components[2].parsedType).toEqual({
|
||||
name: 'button',
|
||||
version: 'chakra_ui/v1',
|
||||
});
|
||||
});
|
||||
});
|
@ -1,11 +1,10 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { createApplication } from '@sunmao-ui/core';
|
||||
import React, { useRef } from 'react';
|
||||
import { initStateAndMethod } from './utils/initStateAndMethod';
|
||||
import { ImplWrapper } from './services/ImplWrapper';
|
||||
import { resolveAppComponents } from './services/resolveAppComponents';
|
||||
import { ImplWrapper } from './components/_internal/ImplWrapper';
|
||||
import { AppProps, UIServices } from './types/RuntimeSchema';
|
||||
import { DebugEvent, DebugStore } from './services/DebugComponents';
|
||||
import { getSlotWithMap } from './components/_internal/Slot';
|
||||
import { RuntimeAppSchemaManager } from './services/RuntimeAppSchemaManager';
|
||||
import { resolveChildrenMap } from './utils/resolveChildrenMap';
|
||||
|
||||
// inject modules to App
|
||||
export function genApp(services: UIServices) {
|
||||
@ -23,33 +22,20 @@ export const App: React.FC<AppProps> = props => {
|
||||
debugStore = true,
|
||||
debugEvent = true,
|
||||
} = props;
|
||||
const app = createApplication(options);
|
||||
|
||||
const runtimeAppSchemaManager = useRef(new RuntimeAppSchemaManager());
|
||||
const app = runtimeAppSchemaManager.current.update(options);
|
||||
initStateAndMethod(services.registry, services.stateManager, app.spec.components);
|
||||
|
||||
const { topLevelComponents, slotComponentsMap } = useMemo(
|
||||
() =>
|
||||
resolveAppComponents(app.spec.components, {
|
||||
services,
|
||||
app,
|
||||
componentWrapper,
|
||||
gridCallbacks,
|
||||
}),
|
||||
[app, componentWrapper, gridCallbacks, services]
|
||||
);
|
||||
|
||||
const { childrenMap, topLevelComponents } = resolveChildrenMap(app.spec.components);
|
||||
return (
|
||||
<div className="App" style={{ height: '100vh', overflow: 'auto' }}>
|
||||
{topLevelComponents.map(c => {
|
||||
const slotsMap = slotComponentsMap.get(c.id);
|
||||
const Slot = getSlotWithMap(slotsMap);
|
||||
return (
|
||||
<ImplWrapper
|
||||
key={c.id}
|
||||
component={c}
|
||||
services={services}
|
||||
slotsMap={slotsMap}
|
||||
Slot={Slot}
|
||||
targetSlot={null}
|
||||
childrenMap={childrenMap}
|
||||
app={app}
|
||||
componentWrapper={componentWrapper}
|
||||
gridCallbacks={gridCallbacks}
|
||||
|
243
packages/runtime/src/components/_internal/ImplWrapper.tsx
Normal file
243
packages/runtime/src/components/_internal/ImplWrapper.tsx
Normal file
@ -0,0 +1,243 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { merge } from 'lodash-es';
|
||||
import { watch } from '../../utils/watchReactivity';
|
||||
import {
|
||||
RuntimeApplicationComponent,
|
||||
ImplWrapperProps,
|
||||
TraitResult,
|
||||
} from '../../types/RuntimeSchema';
|
||||
import { shallowCompareArray } from '../../utils/shallowCompareArray';
|
||||
|
||||
type ArrayElement<ArrayType extends readonly unknown[]> =
|
||||
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
||||
|
||||
type ApplicationTrait = ArrayElement<RuntimeApplicationComponent['traits']>;
|
||||
|
||||
const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props, ref) => {
|
||||
const {
|
||||
component: c,
|
||||
app,
|
||||
children,
|
||||
componentWrapper: ComponentWrapper,
|
||||
services,
|
||||
childrenMap,
|
||||
} = props;
|
||||
const { registry, stateManager, globalHandlerMap, apiService } = props.services;
|
||||
const childrenCache = new Map<RuntimeApplicationComponent, React.ReactElement>();
|
||||
|
||||
const Impl = registry.getComponent(c.parsedType.version, c.parsedType.name).impl;
|
||||
|
||||
if (!globalHandlerMap.has(c.id)) {
|
||||
globalHandlerMap.set(c.id, {});
|
||||
}
|
||||
|
||||
const handlerMap = useRef(globalHandlerMap.get(c.id)!);
|
||||
useEffect(() => {
|
||||
const handler = (s: { componentId: string; name: string; parameters?: any }) => {
|
||||
if (s.componentId !== c.id) {
|
||||
return;
|
||||
}
|
||||
if (!handlerMap.current[s.name]) {
|
||||
// maybe log?
|
||||
return;
|
||||
}
|
||||
handlerMap.current[s.name](s.parameters);
|
||||
};
|
||||
apiService.on('uiMethod', handler);
|
||||
return () => {
|
||||
apiService.off('uiMethod', handler);
|
||||
globalHandlerMap.delete(c.id);
|
||||
};
|
||||
}, [apiService, c.id, globalHandlerMap, handlerMap]);
|
||||
|
||||
const mergeState = useCallback(
|
||||
(partial: any) => {
|
||||
stateManager.store[c.id] = { ...stateManager.store[c.id], ...partial };
|
||||
},
|
||||
[c.id, stateManager.store]
|
||||
);
|
||||
const subscribeMethods = useCallback(
|
||||
(map: any) => {
|
||||
handlerMap.current = { ...handlerMap, ...map };
|
||||
globalHandlerMap.set(c.id, handlerMap.current);
|
||||
},
|
||||
[c.id, globalHandlerMap]
|
||||
);
|
||||
|
||||
const excecuteTrait = useCallback(
|
||||
(trait: ApplicationTrait, traitProperty: ApplicationTrait['properties']) => {
|
||||
const tImpl = registry.getTrait(
|
||||
trait.parsedType.version,
|
||||
trait.parsedType.name
|
||||
).impl;
|
||||
return tImpl({
|
||||
...traitProperty,
|
||||
componentId: c.id,
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
services,
|
||||
});
|
||||
},
|
||||
[c.id, mergeState, registry, services, subscribeMethods]
|
||||
);
|
||||
|
||||
// result returned from traits
|
||||
const [traitResults, setTraitResults] = useState<TraitResult<string, string>[]>(() => {
|
||||
return c.traits.map(trait =>
|
||||
excecuteTrait(trait, stateManager.deepEval(trait.properties).result)
|
||||
);
|
||||
});
|
||||
|
||||
// eval traits' properties then excecute traits
|
||||
useEffect(() => {
|
||||
const stops: ReturnType<typeof watch>[] = [];
|
||||
const properties: Array<ApplicationTrait['properties']> = [];
|
||||
c.traits.forEach((t, i) => {
|
||||
const { result, stop } = stateManager.deepEval(
|
||||
t.properties,
|
||||
({ result: property }: any) => {
|
||||
const traitResult = excecuteTrait(t, property);
|
||||
setTraitResults(oldResults => {
|
||||
// assume traits number and order will not change
|
||||
const newResults = [...oldResults];
|
||||
newResults[i] = traitResult;
|
||||
return newResults;
|
||||
});
|
||||
stops.push(stop);
|
||||
}
|
||||
);
|
||||
properties.push(result);
|
||||
});
|
||||
// although traitResults has initialized in useState, it must be set here again
|
||||
// because mergeState will be called during the first render of component, and state will change
|
||||
setTraitResults(c.traits.map((trait, i) => excecuteTrait(trait, properties[i])));
|
||||
return () => stops.forEach(s => s());
|
||||
}, [c.traits, excecuteTrait, stateManager]);
|
||||
|
||||
// reduce traitResults
|
||||
const propsFromTraits: TraitResult<string, string>['props'] = useMemo(() => {
|
||||
return Array.from(traitResults.values()).reduce(
|
||||
(prevProps, result: TraitResult<string, string>) => {
|
||||
if (!result.props) {
|
||||
return prevProps;
|
||||
}
|
||||
|
||||
let effects = prevProps?.effects || [];
|
||||
if (result.props?.effects) {
|
||||
effects = effects?.concat(result.props?.effects);
|
||||
}
|
||||
|
||||
return merge(prevProps, result.props, { effects });
|
||||
},
|
||||
{} as TraitResult<string, string>['props']
|
||||
);
|
||||
}, [traitResults]);
|
||||
const unmount = traitResults.some(r => r.unmount);
|
||||
|
||||
// component properties
|
||||
const [evaledComponentProperties, setEvaledComponentProperties] = useState(() => {
|
||||
return merge(stateManager.deepEval(c.properties).result, propsFromTraits);
|
||||
});
|
||||
|
||||
// eval component properties
|
||||
useEffect(() => {
|
||||
const { result, stop } = stateManager.deepEval(
|
||||
c.properties,
|
||||
({ result: newResult }: any) => {
|
||||
setEvaledComponentProperties({ ...newResult });
|
||||
}
|
||||
);
|
||||
// must keep this line, reason is the same as above
|
||||
setEvaledComponentProperties({ ...result });
|
||||
return stop;
|
||||
}, [c.properties, stateManager]);
|
||||
|
||||
const mergedProps = { ...evaledComponentProperties, ...propsFromTraits };
|
||||
|
||||
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}
|
||||
slotsElements={genSlotsElements()}
|
||||
mergeState={mergeState}
|
||||
subscribeMethods={subscribeMethods}
|
||||
/>
|
||||
);
|
||||
|
||||
let result = (
|
||||
<React.Fragment key={c.id}>
|
||||
{C}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
let parentComponent;
|
||||
|
||||
const slotTrait = c.traits.find(t => t.type === 'core/v1/slot');
|
||||
|
||||
if (slotTrait && app) {
|
||||
parentComponent = app.spec.components.find(
|
||||
c => c.id === (slotTrait.properties.container as any).id
|
||||
);
|
||||
}
|
||||
// wrap component, but grid_layout is root component and cannot be chosen, so don't wrap it
|
||||
if (
|
||||
ComponentWrapper &&
|
||||
c.parsedType.name !== 'dummy' &&
|
||||
c.parsedType.name !== 'grid_layout'
|
||||
) {
|
||||
result = (
|
||||
<ComponentWrapper component={c} parentType={parentComponent?.type || ''}>
|
||||
{result}
|
||||
</ComponentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (parentComponent?.parsedType.name === 'grid_layout') {
|
||||
// prevent react componentWrapper
|
||||
/* eslint-disable */
|
||||
const { component, services, app, componentWrapper, gridCallbacks, ...restProps } =
|
||||
props;
|
||||
/* eslint-enable */
|
||||
result = (
|
||||
<div key={c.id} data-sunmao-ui-id={c.id} ref={ref} {...restProps}>
|
||||
{result}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
export const ImplWrapper = React.memo<ImplWrapperProps>(
|
||||
_ImplWrapper,
|
||||
(prevProps, nextProps) => {
|
||||
const prevChildren = prevProps.childrenMap[prevProps.component.id]?._grandChildren;
|
||||
const nextChildren = nextProps.childrenMap[nextProps.component.id]?._grandChildren;
|
||||
if (!prevChildren || !nextProps) return false;
|
||||
let isEqual = false;
|
||||
|
||||
if (prevChildren && nextChildren) {
|
||||
isEqual = shallowCompareArray(prevChildren, nextChildren);
|
||||
}
|
||||
return isEqual;
|
||||
}
|
||||
);
|
@ -2,15 +2,17 @@ import { Static } from '@sinclair/typebox';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { get } from 'lodash-es';
|
||||
import { useDeepCompareMemo } from 'use-deep-compare';
|
||||
import { RuntimeApplication } from '@sunmao-ui/core';
|
||||
import { UIServices, RuntimeModuleSchema } from '../../types/RuntimeSchema';
|
||||
import { Application, parseType, RuntimeApplication } from '@sunmao-ui/core';
|
||||
import {
|
||||
UIServices,
|
||||
RuntimeModuleSchema,
|
||||
RuntimeApplicationComponent,
|
||||
} from '../../types/RuntimeSchema';
|
||||
import { EventHandlerSchema } from '../../types/TraitPropertiesSchema';
|
||||
import { resolveAppComponents } from '../../services/resolveAppComponents';
|
||||
import { ImplWrapper } from '../../services/ImplWrapper';
|
||||
import { ImplWrapper } from './ImplWrapper';
|
||||
import { watch } from '../../utils/watchReactivity';
|
||||
import { parseTypeComponents } from '../../utils/parseType';
|
||||
import { ImplementedRuntimeModule } from '../../services/registry';
|
||||
import { getSlotWithMap } from './Slot';
|
||||
import { resolveChildrenMap } from '../../utils/resolveChildrenMap';
|
||||
|
||||
type Props = Static<typeof RuntimeModuleSchema> & {
|
||||
evalScope?: Record<string, any>;
|
||||
@ -46,20 +48,20 @@ const ModuleRendererContent: React.FC<Props & { moduleSpec: ImplementedRuntimeMo
|
||||
}).obj;
|
||||
}
|
||||
|
||||
const evalWithScope = useCallback(<T extends Record<string, any>>(
|
||||
obj: T,
|
||||
scope: Record<string, any>
|
||||
): T =>{
|
||||
const hasScopeKey = (exp: string) => {
|
||||
return Object.keys(scope).some(key => exp.includes('{{') && exp.includes(key));
|
||||
};
|
||||
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
|
||||
if (typeof value === 'string' && hasScopeKey(value)) {
|
||||
return services.stateManager.maskedEval(value, true, scope);
|
||||
}
|
||||
return value;
|
||||
}).obj;
|
||||
}, [services.stateManager]);
|
||||
const evalWithScope = useCallback(
|
||||
<T extends Record<string, any>>(obj: T, scope: Record<string, any>): T => {
|
||||
const hasScopeKey = (exp: string) => {
|
||||
return Object.keys(scope).some(key => exp.includes('{{') && exp.includes(key));
|
||||
};
|
||||
return services.stateManager.mapValuesDeep({ obj }, ({ value }) => {
|
||||
if (typeof value === 'string' && hasScopeKey(value)) {
|
||||
return services.stateManager.maskedEval(value, true, scope);
|
||||
}
|
||||
return value;
|
||||
}).obj;
|
||||
},
|
||||
[services.stateManager]
|
||||
);
|
||||
|
||||
// first eval the property, handlers, id of module
|
||||
const evaledProperties = evalObject(properties);
|
||||
@ -146,25 +148,15 @@ const ModuleRendererContent: React.FC<Props & { moduleSpec: ImplementedRuntimeMo
|
||||
}, [evaledHanlders, moduleId, services.apiService]);
|
||||
|
||||
const result = useMemo(() => {
|
||||
const { topLevelComponents, slotComponentsMap } = resolveAppComponents(
|
||||
evaledModuleTemplate,
|
||||
{
|
||||
services,
|
||||
app,
|
||||
}
|
||||
);
|
||||
const { childrenMap, topLevelComponents } = resolveChildrenMap(evaledModuleTemplate);
|
||||
return topLevelComponents.map(c => {
|
||||
const slotsMap = slotComponentsMap.get(c.id);
|
||||
const Slot = getSlotWithMap(slotsMap);
|
||||
return (
|
||||
<ImplWrapper
|
||||
key={c.id}
|
||||
component={c}
|
||||
slotsMap={slotsMap}
|
||||
Slot={Slot}
|
||||
targetSlot={null}
|
||||
services={services}
|
||||
app={app}
|
||||
childrenMap={childrenMap}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -172,3 +164,18 @@ const ModuleRendererContent: React.FC<Props & { moduleSpec: ImplementedRuntimeMo
|
||||
|
||||
return <>{result}</>;
|
||||
};
|
||||
|
||||
function parseTypeComponents(
|
||||
c: Application['spec']['components'][0]
|
||||
): RuntimeApplicationComponent {
|
||||
return {
|
||||
...c,
|
||||
parsedType: parseType(c.type),
|
||||
traits: c.traits.map(t => {
|
||||
return {
|
||||
...t,
|
||||
parsedType: parseType(t.type),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { SlotsMap } from '../../types/RuntimeSchema';
|
||||
|
||||
export type SlotType<K extends string> = React.FC<{
|
||||
slot: K;
|
||||
}>;
|
||||
|
||||
export function getSlots<T, K extends string>(
|
||||
slotsMap: SlotsMap<K> | undefined,
|
||||
slot: K,
|
||||
rest: T
|
||||
): React.ReactElement[] {
|
||||
const components = slotsMap?.get(slot);
|
||||
if (!components) {
|
||||
const placeholder = (
|
||||
<div key="slot-placeholder" style={{ color: 'gray' }}>
|
||||
Slot {slot} is empty.Please drag component to this slot.
|
||||
</div>
|
||||
);
|
||||
return [placeholder];
|
||||
}
|
||||
return components.map(({ component: ImplWrapper, id }) => (
|
||||
<ImplWrapper key={id} {...rest} />
|
||||
));
|
||||
}
|
||||
|
||||
function Slot<K extends string>({
|
||||
slotsMap,
|
||||
slot,
|
||||
...rest
|
||||
}: {
|
||||
slotsMap: SlotsMap<K> | undefined;
|
||||
slot: K;
|
||||
}): ReturnType<React.FC> {
|
||||
if (!slotsMap?.has(slot)) {
|
||||
return null;
|
||||
}
|
||||
return <>{getSlots(slotsMap, slot, rest)}</>;
|
||||
}
|
||||
|
||||
export function getSlotWithMap<K extends string>(
|
||||
slotsMap: SlotsMap<K> | undefined
|
||||
): SlotType<K> {
|
||||
return props => <Slot slotsMap={slotsMap} {...props} />;
|
||||
}
|
||||
|
||||
export default Slot;
|
@ -1,6 +1,5 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { implementRuntimeComponent } from '../../utils/buildKit';
|
||||
import { getSlots } from '../_internal/Slot';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { partial } from 'lodash-es';
|
||||
import { css } from '@emotion/css';
|
||||
@ -41,7 +40,7 @@ export default implementRuntimeComponent({
|
||||
styleSlots: ['content'],
|
||||
events: [],
|
||||
},
|
||||
})(({ slotsMap, layout = [], gridCallbacks, component, customStyle }) => {
|
||||
})(({ layout = [], gridCallbacks, component, customStyle, slotsElements }) => {
|
||||
const onDragStop = gridCallbacks?.onDragStop
|
||||
? partial(gridCallbacks.onDragStop, component.id)
|
||||
: undefined;
|
||||
@ -61,7 +60,7 @@ export default implementRuntimeComponent({
|
||||
${customStyle?.content}
|
||||
`}
|
||||
>
|
||||
{getSlots(slotsMap, 'content', {})}
|
||||
{slotsElements.content}
|
||||
</BaseGridLayout>
|
||||
</Suspense>
|
||||
);
|
||||
|
@ -21,7 +21,11 @@ import {
|
||||
RouterCtx,
|
||||
useNavigate,
|
||||
} from './hooks';
|
||||
import { SlotsMap } from '../../../types/RuntimeSchema';
|
||||
import {
|
||||
RuntimeApplicationComponent,
|
||||
UIServices,
|
||||
ChildrenMap,
|
||||
} from '../../../types/RuntimeSchema';
|
||||
|
||||
export type RouteLikeElement = PropsWithChildren<{
|
||||
path?: string;
|
||||
@ -51,7 +55,7 @@ export const Route: React.FC<RouteProps> = ({ match, children, mergeState }) =>
|
||||
}
|
||||
mergeState(destroyObj);
|
||||
};
|
||||
}, [params]);
|
||||
}, [matches, mergeState, params]);
|
||||
if (!matches) return null;
|
||||
return typeof children === 'function' ? children(params) : children;
|
||||
};
|
||||
@ -59,18 +63,24 @@ export const Route: React.FC<RouteProps> = ({ match, children, mergeState }) =>
|
||||
type SwitchProps = {
|
||||
location?: string;
|
||||
switchPolicy: SwitchPolicy;
|
||||
slotMap?: SlotsMap<string>;
|
||||
component: RuntimeApplicationComponent;
|
||||
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> = ({
|
||||
switchPolicy,
|
||||
location,
|
||||
slotMap,
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
}) => {
|
||||
export const Switch: React.FC<SwitchProps> = props => {
|
||||
const {
|
||||
switchPolicy,
|
||||
location,
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
slotsElements,
|
||||
childrenMap,
|
||||
component,
|
||||
} = props;
|
||||
const [originalLocation] = useLocation();
|
||||
const matcher = useMemo(() => makeMatcher(), []);
|
||||
|
||||
@ -80,7 +90,7 @@ export const Switch: React.FC<SwitchProps> = ({
|
||||
let defaultPath: string | undefined = undefined;
|
||||
const result = switchPolicy.map(
|
||||
({ type, path, slotId, href, default: _default, exact, strict, sensitive }) => {
|
||||
const componentsArr = slotMap && slotMap.get(slotId);
|
||||
const children = slotsElements[slotId];
|
||||
if (defaultPath === undefined && _default) {
|
||||
defaultPath = path;
|
||||
}
|
||||
@ -99,19 +109,19 @@ export const Switch: React.FC<SwitchProps> = ({
|
||||
</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 { component: 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>
|
||||
);
|
||||
}
|
||||
@ -124,7 +134,7 @@ export const Switch: React.FC<SwitchProps> = ({
|
||||
path={path}
|
||||
mergeState={mergeState}
|
||||
>
|
||||
<C key={slotId}></C>
|
||||
{ele}
|
||||
</Route>
|
||||
);
|
||||
default:
|
||||
@ -141,7 +151,7 @@ export const Switch: React.FC<SwitchProps> = ({
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, [switchPolicy]);
|
||||
}, [component.id, mergeState, slotsElements, switchPolicy, childrenMap]);
|
||||
|
||||
useEffect(() => {
|
||||
subscribeMethods({
|
||||
@ -149,7 +159,7 @@ export const Switch: React.FC<SwitchProps> = ({
|
||||
naviagte(path);
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
}, [naviagte, subscribeMethods]);
|
||||
|
||||
useEffect(() => {
|
||||
// to assign location as a state
|
||||
@ -161,7 +171,7 @@ export const Switch: React.FC<SwitchProps> = ({
|
||||
route: undefined,
|
||||
});
|
||||
};
|
||||
}, [loc]);
|
||||
}, [loc, mergeState]);
|
||||
|
||||
for (const element of flattenChildren(routes)) {
|
||||
const match: Match<DefaultParams> = element.props.path
|
||||
@ -228,7 +238,7 @@ export const Redirect: React.FC<RedirectProps> = props => {
|
||||
// empty array means running the effect once, navRef is a ref so it never changes
|
||||
useLayoutEffect(() => {
|
||||
navRef.current!();
|
||||
}, []);
|
||||
}, [navRef]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -59,13 +59,10 @@ export default implementRuntimeComponent({
|
||||
styleSlots: [],
|
||||
events: [],
|
||||
},
|
||||
})(({ slotsMap, switchPolicy, subscribeMethods, mergeState }) => {
|
||||
})((props) => {
|
||||
return (
|
||||
<Switch
|
||||
slotMap={slotsMap}
|
||||
switchPolicy={switchPolicy}
|
||||
subscribeMethods={subscribeMethods}
|
||||
mergeState={mergeState}
|
||||
{...props}
|
||||
></Switch>
|
||||
);
|
||||
});
|
||||
|
@ -22,7 +22,6 @@ export function initSunmaoUI(dependencies = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export * from './utils/parseType';
|
||||
export * from './utils/parseTypeBox';
|
||||
export * from './utils/buildKit';
|
||||
export * from './utils/encodeDragDataTransfer';
|
||||
@ -30,7 +29,6 @@ export * from './types/RuntimeSchema';
|
||||
export * from './types/TraitPropertiesSchema';
|
||||
export * from './constants';
|
||||
export * from './services/registry';
|
||||
export { default as Slot, getSlots } from './components/_internal/Slot';
|
||||
export { ModuleRenderer } from './components/_internal/ModuleRenderer';
|
||||
export { default as Text, TextPropertySchema } from './components/_internal/Text';
|
||||
|
||||
|
@ -1,220 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { watch } from '../utils/watchReactivity';
|
||||
import { merge } from 'lodash-es';
|
||||
import {
|
||||
RuntimeApplicationComponent,
|
||||
ImplWrapperProps,
|
||||
TraitResult,
|
||||
} from '../types/RuntimeSchema';
|
||||
import { getSlotWithMap } from '../components/_internal/Slot';
|
||||
|
||||
type ArrayElement<ArrayType extends readonly unknown[]> =
|
||||
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
||||
|
||||
type ApplicationTrait = ArrayElement<RuntimeApplicationComponent['traits']>;
|
||||
|
||||
export const ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
component: c,
|
||||
targetSlot,
|
||||
app,
|
||||
children,
|
||||
componentWrapper: ComponentWrapper,
|
||||
services,
|
||||
} = props;
|
||||
|
||||
const { registry, stateManager, globalHandlerMap, apiService } = props.services;
|
||||
|
||||
const Impl = registry.getComponent(c.parsedType.version, c.parsedType.name).impl;
|
||||
|
||||
if (!globalHandlerMap.has(c.id)) {
|
||||
globalHandlerMap.set(c.id, {});
|
||||
}
|
||||
|
||||
const handlerMap = useRef(globalHandlerMap.get(c.id)!);
|
||||
useEffect(() => {
|
||||
const handler = (s: { componentId: string; name: string; parameters?: any }) => {
|
||||
if (s.componentId !== c.id) {
|
||||
return;
|
||||
}
|
||||
if (!handlerMap.current[s.name]) {
|
||||
// maybe log?
|
||||
return;
|
||||
}
|
||||
handlerMap.current[s.name](s.parameters);
|
||||
};
|
||||
apiService.on('uiMethod', handler);
|
||||
return () => {
|
||||
apiService.off('uiMethod', handler);
|
||||
globalHandlerMap.delete(c.id);
|
||||
};
|
||||
}, [apiService, c.id, globalHandlerMap, handlerMap]);
|
||||
|
||||
const mergeState = useCallback(
|
||||
(partial: any) => {
|
||||
stateManager.store[c.id] = { ...stateManager.store[c.id], ...partial };
|
||||
},
|
||||
[c.id, stateManager.store]
|
||||
);
|
||||
const subscribeMethods = useCallback(
|
||||
(map: any) => {
|
||||
handlerMap.current = { ...handlerMap, ...map };
|
||||
globalHandlerMap.set(c.id, handlerMap.current);
|
||||
},
|
||||
[c.id, globalHandlerMap]
|
||||
);
|
||||
|
||||
const excecuteTrait = useCallback(
|
||||
(trait: ApplicationTrait, traitProperty: ApplicationTrait['properties']) => {
|
||||
const tImpl = registry.getTrait(
|
||||
trait.parsedType.version,
|
||||
trait.parsedType.name
|
||||
).impl;
|
||||
return tImpl({
|
||||
...traitProperty,
|
||||
componentId: c.id,
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
services,
|
||||
});
|
||||
},
|
||||
[c.id, mergeState, registry, services, subscribeMethods]
|
||||
);
|
||||
|
||||
// result returned from traits
|
||||
const [traitResults, setTraitResults] = useState<TraitResult<string, string>[]>(
|
||||
() => {
|
||||
return c.traits.map(trait =>
|
||||
excecuteTrait(trait, stateManager.deepEval(trait.properties).result)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// eval traits' properties then excecute traits
|
||||
useEffect(() => {
|
||||
const stops: ReturnType<typeof watch>[] = [];
|
||||
const properties: Array<ApplicationTrait['properties']> = [];
|
||||
c.traits.forEach((t, i) => {
|
||||
const { result, stop } = stateManager.deepEval(
|
||||
t.properties,
|
||||
({ result: property }: any) => {
|
||||
const traitResult = excecuteTrait(t, property);
|
||||
setTraitResults(oldResults => {
|
||||
// assume traits number and order will not change
|
||||
const newResults = [...oldResults];
|
||||
newResults[i] = traitResult;
|
||||
return newResults;
|
||||
});
|
||||
stops.push(stop);
|
||||
}
|
||||
);
|
||||
properties.push(result);
|
||||
});
|
||||
// although traitResults has initialized in useState, it must be set here again
|
||||
// because mergeState will be called during the first render of component, and state will change
|
||||
setTraitResults(c.traits.map((trait, i) => excecuteTrait(trait, properties[i])));
|
||||
return () => stops.forEach(s => s());
|
||||
}, [c.traits, excecuteTrait, stateManager]);
|
||||
|
||||
// reduce traitResults
|
||||
const propsFromTraits: TraitResult<string, string>['props'] = useMemo(() => {
|
||||
return Array.from(traitResults.values()).reduce(
|
||||
(prevProps, result: TraitResult<string, string>) => {
|
||||
if (!result.props) {
|
||||
return prevProps;
|
||||
}
|
||||
|
||||
let effects = prevProps?.effects || [];
|
||||
if (result.props?.effects) {
|
||||
effects = effects?.concat(result.props?.effects);
|
||||
}
|
||||
|
||||
return merge(prevProps, result.props, { effects });
|
||||
},
|
||||
{} as TraitResult<string, string>['props']
|
||||
);
|
||||
}, [traitResults]);
|
||||
const unmount = traitResults.some(r => r.unmount);
|
||||
|
||||
// component properties
|
||||
const [evaledComponentProperties, setEvaledComponentProperties] = useState(() => {
|
||||
return merge(stateManager.deepEval(c.properties).result, propsFromTraits);
|
||||
});
|
||||
|
||||
// eval component properties
|
||||
useEffect(() => {
|
||||
const { result, stop } = stateManager.deepEval(
|
||||
c.properties,
|
||||
({ result: newResult }: any) => {
|
||||
setEvaledComponentProperties({ ...newResult });
|
||||
}
|
||||
);
|
||||
// must keep this line, reason is the same as above
|
||||
setEvaledComponentProperties({ ...result });
|
||||
return stop;
|
||||
}, [c.properties, stateManager]);
|
||||
|
||||
const mergedProps = { ...evaledComponentProperties, ...propsFromTraits };
|
||||
const { slotsMap, ...restProps } = props;
|
||||
const Slot = getSlotWithMap(slotsMap);
|
||||
const C = unmount ? null : (
|
||||
<Impl
|
||||
key={c.id}
|
||||
{...mergedProps}
|
||||
{...restProps}
|
||||
Slot={Slot}
|
||||
slotsMap={slotsMap}
|
||||
mergeState={mergeState}
|
||||
subscribeMethods={subscribeMethods}
|
||||
/>
|
||||
);
|
||||
|
||||
let result = (
|
||||
<React.Fragment key={c.id}>
|
||||
{C}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
let parentComponent;
|
||||
if (targetSlot && app) {
|
||||
parentComponent = app.spec.components.find(c => c.id === targetSlot.id);
|
||||
}
|
||||
// wrap component, but grid_layout is root component and cannot be chosen, so don't wrap it
|
||||
if (
|
||||
ComponentWrapper &&
|
||||
c.parsedType.name !== 'dummy' &&
|
||||
c.parsedType.name !== 'grid_layout'
|
||||
) {
|
||||
result = (
|
||||
<ComponentWrapper component={c} parentType={parentComponent?.type || ''}>
|
||||
{result}
|
||||
</ComponentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (parentComponent?.parsedType.name === 'grid_layout') {
|
||||
// prevent react componentWrapper
|
||||
/* eslint-disable */
|
||||
const {
|
||||
component,
|
||||
services,
|
||||
targetSlot,
|
||||
app,
|
||||
slotsMap,
|
||||
componentWrapper,
|
||||
gridCallbacks,
|
||||
...restProps
|
||||
} = props;
|
||||
/* eslint-enable */
|
||||
result = (
|
||||
<div key={c.id} data-sunmao-ui-id={c.id} ref={ref} {...restProps}>
|
||||
{result}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
50
packages/runtime/src/services/RuntimeAppSchemaManager.ts
Normal file
50
packages/runtime/src/services/RuntimeAppSchemaManager.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import {
|
||||
Application,
|
||||
ApplicationComponent,
|
||||
RuntimeApplication,
|
||||
isValidId,
|
||||
parseType,
|
||||
parseVersion,
|
||||
} from '@sunmao-ui/core';
|
||||
import { RuntimeApplicationComponent } from '../types/RuntimeSchema';
|
||||
export class RuntimeAppSchemaManager {
|
||||
private runtimeComponentsCache: Record<string, RuntimeApplicationComponent> = {};
|
||||
private componentsCache: Record<string, ApplicationComponent> = {};
|
||||
|
||||
update(schema: Application): RuntimeApplication {
|
||||
return {
|
||||
...schema,
|
||||
kind: 'Application',
|
||||
parsedVersion: parseVersion(schema.version),
|
||||
spec: {
|
||||
...schema.spec,
|
||||
components: schema.spec.components.map(c => {
|
||||
return this.genComponent(c);
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
genComponent(component: ApplicationComponent): RuntimeApplicationComponent {
|
||||
const componentInCache = this.componentsCache[component.id];
|
||||
if (componentInCache && componentInCache === component) {
|
||||
return this.runtimeComponentsCache[component.id];
|
||||
}
|
||||
if (!isValidId(component.id)) {
|
||||
throw new Error(`Invalid id: "${component.id}"`);
|
||||
}
|
||||
const componentSchema: RuntimeApplicationComponent = {
|
||||
...component,
|
||||
parsedType: parseType(component.type),
|
||||
traits: component.traits.map(t => {
|
||||
return {
|
||||
...t,
|
||||
parsedType: parseType(t.type),
|
||||
};
|
||||
}),
|
||||
};
|
||||
this.runtimeComponentsCache[component.id] = componentSchema;
|
||||
this.componentsCache[component.id] = component;
|
||||
return componentSchema;
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import {
|
||||
RuntimeModuleSpec,
|
||||
ApplicationComponent,
|
||||
RuntimeComponentSpec,
|
||||
parseType,
|
||||
} from '@sunmao-ui/core';
|
||||
// components
|
||||
/* --- plain --- */
|
||||
@ -26,7 +27,6 @@ import {
|
||||
ComponentImplementationProps,
|
||||
TraitImplementation,
|
||||
} from '../types/RuntimeSchema';
|
||||
import { parseType } from '../utils/parseType';
|
||||
import { parseModuleSchema } from '../utils/parseModuleSchema';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
|
@ -1,65 +0,0 @@
|
||||
import React from 'react';
|
||||
import { RuntimeApplication } from '@sunmao-ui/core';
|
||||
import { ContainerPropertySchema } from '../traits/core/slot';
|
||||
import { Static } from '@sinclair/typebox';
|
||||
import {
|
||||
ComponentParamsFromApp,
|
||||
UIServices,
|
||||
SlotComponentMap,
|
||||
} from '../types/RuntimeSchema';
|
||||
import { ImplWrapper } from './ImplWrapper';
|
||||
|
||||
export function resolveAppComponents(
|
||||
components: RuntimeApplication['spec']['components'],
|
||||
params: {
|
||||
services: UIServices;
|
||||
app?: RuntimeApplication;
|
||||
} & ComponentParamsFromApp
|
||||
): {
|
||||
topLevelComponents: RuntimeApplication['spec']['components'];
|
||||
slotComponentsMap: SlotComponentMap;
|
||||
} {
|
||||
const topLevelComponents: RuntimeApplication['spec']['components'] = [];
|
||||
const slotComponentsMap: SlotComponentMap = new Map();
|
||||
|
||||
for (const c of components) {
|
||||
// handle component with slot trait
|
||||
const slotTrait = c.traits.find(t => t.parsedType.name === 'slot');
|
||||
if (slotTrait) {
|
||||
const { id, slot } = (
|
||||
slotTrait.properties as {
|
||||
container: Static<typeof ContainerPropertySchema>;
|
||||
}
|
||||
).container;
|
||||
if (!slotComponentsMap.has(id)) {
|
||||
slotComponentsMap.set(id, new Map());
|
||||
}
|
||||
if (!slotComponentsMap.get(id)?.has(slot)) {
|
||||
slotComponentsMap.get(id)?.set(slot, []);
|
||||
}
|
||||
const component = React.forwardRef<HTMLDivElement, any>((props, ref) => (
|
||||
<ImplWrapper
|
||||
component={c}
|
||||
slotsMap={slotComponentsMap.get(c.id)}
|
||||
targetSlot={{ id, slot }}
|
||||
{...params}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
component.displayName = c.parsedType.name;
|
||||
slotComponentsMap.get(id)?.get(slot)?.push({
|
||||
component,
|
||||
id: c.id,
|
||||
});
|
||||
}
|
||||
|
||||
// if the component is neither assigned with slot trait nor route trait, consider it as a top level component
|
||||
!slotTrait && topLevelComponents.push(c);
|
||||
}
|
||||
|
||||
return {
|
||||
topLevelComponents,
|
||||
slotComponentsMap,
|
||||
};
|
||||
}
|
@ -6,7 +6,7 @@ import { StateManager } from '../services/stateStore';
|
||||
import { Application, RuntimeApplication } from '@sunmao-ui/core';
|
||||
import { EventHandlerSchema } from './TraitPropertiesSchema';
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import { SlotType } from '../components/_internal/Slot';
|
||||
import React from 'react';
|
||||
|
||||
export type RuntimeApplicationComponent = RuntimeApplication['spec']['components'][0];
|
||||
|
||||
@ -44,21 +44,17 @@ export type AppProps = {
|
||||
// TODO: (type-safe), remove fallback type
|
||||
export type ImplWrapperProps<KSlot extends string = string> = {
|
||||
component: RuntimeApplicationComponent;
|
||||
// TODO: (type-safe), remove slotsMap from props
|
||||
slotsMap: SlotsMap<KSlot> | undefined;
|
||||
Slot: SlotType<KSlot>;
|
||||
targetSlot: { id: string; slot: string } | null;
|
||||
childrenMap: ChildrenMap<KSlot>;
|
||||
services: UIServices;
|
||||
app?: RuntimeApplication;
|
||||
} & ComponentParamsFromApp;
|
||||
|
||||
export type SlotComponentMap = Map<string, SlotsMap<string>>;
|
||||
export type SlotsMap<K extends string> = Map<
|
||||
K,
|
||||
Array<{
|
||||
component: React.FC;
|
||||
id: string;
|
||||
}>
|
||||
export type ChildrenMap<KSlot extends string> = Record<
|
||||
string,
|
||||
Record<KSlot, RuntimeApplicationComponent[]> & {
|
||||
_grandChildren?: RuntimeApplicationComponent[];
|
||||
_allChildren: RuntimeApplicationComponent[];
|
||||
}
|
||||
>;
|
||||
|
||||
export type CallbackMap<K extends string> = Record<K, () => void>;
|
||||
@ -81,7 +77,9 @@ export type ComponentImplementationProps<
|
||||
KEvent extends string
|
||||
> = ImplWrapperProps<KSlot> &
|
||||
TraitResult<KStyleSlot, KEvent>['props'] &
|
||||
RuntimeFunctions<TState, TMethods>;
|
||||
RuntimeFunctions<TState, TMethods> & {
|
||||
slotsElements: Record<KSlot, React.ReactElement[]>
|
||||
};
|
||||
|
||||
export type TraitResult<KStyleSlot extends string, KEvent extends string> = {
|
||||
props: {
|
||||
|
@ -1,34 +0,0 @@
|
||||
import { Application } from '@sunmao-ui/core';
|
||||
import { RuntimeApplicationComponent } from '../types/RuntimeSchema';
|
||||
|
||||
// parse component Type
|
||||
export function parseType(v: string) {
|
||||
const TYPE_REG = /^([a-zA-Z0-9_\d]+\/[a-zA-Z0-9_\d]+)\/([a-zA-Z0-9_\d]+)$/;
|
||||
function isValidType(v: string): boolean {
|
||||
return TYPE_REG.test(v);
|
||||
}
|
||||
if (!isValidType(v)) {
|
||||
throw new Error(`Invalid type string: "${v}"`);
|
||||
}
|
||||
|
||||
const [, version, name] = v.match(TYPE_REG) ?? [];
|
||||
return {
|
||||
version,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseTypeComponents(
|
||||
c: Application['spec']['components'][0]
|
||||
): RuntimeApplicationComponent {
|
||||
return {
|
||||
...c,
|
||||
parsedType: parseType(c.type),
|
||||
traits: c.traits.map(t => {
|
||||
return {
|
||||
...t,
|
||||
parsedType: parseType(t.type),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
62
packages/runtime/src/utils/resolveChildrenMap.ts
Normal file
62
packages/runtime/src/utils/resolveChildrenMap.ts
Normal 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,
|
||||
};
|
||||
}
|
7
packages/runtime/src/utils/shallowCompareArray.ts
Normal file
7
packages/runtime/src/utils/shallowCompareArray.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function shallowCompareArray<T>(arr1: Array<T>, arr2: Array<T>): boolean {
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
for (const i in arr1) {
|
||||
if (arr1[i] !== arr2[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user