Merge branch 'main' of https://github.com/webzard-io/sunmao-ui into feat/shared

This commit is contained in:
MrWindlike 2022-04-25 16:36:08 +08:00
commit bc2b9ae154
27 changed files with 471 additions and 333 deletions

View File

@ -38,6 +38,7 @@
"Content-Type": "application/json"
},
"body": "{{ form.data }}",
"bodyType": "json",
"onComplete": [
{
"componentId": "$utils",

View File

@ -38,6 +38,7 @@
"Content-Type": "application/json"
},
"body": "{{ form.data }}",
"bodyType": "json",
"onComplete": [
{
"componentId": "$utils",

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/arco-lib",
"version": "0.1.7",
"version": "0.1.8",
"homepage": "https://github.com/webzard-io/sunmao-ui-arco-lib",
"license": "MIT",
"publishConfig": {
@ -31,8 +31,8 @@
"@arco-design/web-react": "^2.29.0",
"@emotion/css": "^11.7.1",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/runtime": "^0.5.6",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/runtime": "^0.5.7",
"eslint-plugin-react-hooks": "^4.3.0",
"lodash-es": "^4.17.21",
"react": "^17.0.2",
@ -42,9 +42,9 @@
"devDependencies": {
"@types/lodash": "^4.14.170",
"@types/lodash-es": "^4.17.5",
"@types/react-resizable": "^1.7.4",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-resizable": "^1.7.4",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"@vitejs/plugin-react": "^1.0.0",

View File

@ -51,12 +51,15 @@ export const getComponentProps = <
data,
customStyle,
callbackMap,
effects,
mergeState,
subscribeMethods,
getElement,
elementRef,
hooks,
isInModule,
componentDidMount,
componentDidUnmount,
componentDidUpdate,
/* eslint-enable @typescript-eslint/no-unused-vars */
...rest
} = props;

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/chakra-ui-lib",
"version": "0.3.7",
"version": "0.3.8",
"description": "sunmao-ui chakra-ui library",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",
@ -33,9 +33,9 @@
"@chakra-ui/react": "^1.7.1",
"@emotion/styled": "^11.6.0",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/editor-sdk": "^0.1.8",
"@sunmao-ui/runtime": "^0.5.6",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/editor-sdk": "^0.1.9",
"@sunmao-ui/runtime": "^0.5.7",
"chakra-react-select": "^1.3.2",
"framer-motion": "^4",
"lodash-es": "^4.17.21",

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/core",
"version": "0.5.4",
"version": "0.5.5",
"description": "sunmao-ui core",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/editor-sdk",
"version": "0.1.8",
"version": "0.1.9",
"description": "The SDK for SunMao Editor",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",
@ -29,8 +29,8 @@
"@emotion/css": "^11.7.1",
"@emotion/react": "^11.1.1",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/runtime": "^0.5.6",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/runtime": "^0.5.7",
"@sunmao-ui/shared": "^0.0.0",
"codemirror": "^5.63.3",
"formik": "^2.2.9",

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/editor",
"version": "0.5.8",
"version": "0.5.9",
"description": "sunmao-ui editor",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",
@ -36,11 +36,11 @@
"@emotion/css": "^11.7.1",
"@emotion/react": "^11.1.1",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/arco-lib": "^0.1.7",
"@sunmao-ui/chakra-ui-lib": "^0.3.7",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/editor-sdk": "^0.1.8",
"@sunmao-ui/runtime": "^0.5.6",
"@sunmao-ui/arco-lib": "^0.1.8",
"@sunmao-ui/chakra-ui-lib": "^0.3.8",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/editor-sdk": "^0.1.9",
"@sunmao-ui/runtime": "^0.5.7",
"@sunmao-ui/shared": "^0.0.0",
"acorn": "^8.7.0",
"acorn-loose": "^8.3.0",

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState, useEffect } from 'react';
import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { Application } from '@sunmao-ui/core';
import {
GridCallbacks,
@ -38,7 +38,8 @@ type Props = {
stateStore: ReturnOfInit['stateManager']['store'];
services: EditorServices;
libs: SunmaoLib[];
uiProps: UIPros
onRefresh: () => void;
uiProps: UIPros;
};
const ApiFormStyle = css`
@ -51,7 +52,7 @@ const ApiFormStyle = css`
`;
export const Editor: React.FC<Props> = observer(
({ App, registry, stateStore, services, libs, uiProps }) => {
({ App, registry, stateStore, services, libs, uiProps, onRefresh: onRefreshApp }) => {
const { eventBus, editorStore } = services;
const {
components,
@ -69,12 +70,7 @@ export const Editor: React.FC<Props> = observer(
const [preview, setPreview] = useState(false);
const [codeMode, setCodeMode] = useState(false);
const [code, setCode] = useState('');
const [recoverKey, setRecoverKey] = useState(0);
const [isError, setIsError] = useState<boolean>(false);
const onError = (err: Error | null) => {
setIsError(err !== null);
};
const [isDisplayApp, setIsDisplayApp] = useState(true);
const gridCallbacks: GridCallbacks = useMemo(() => {
return {
@ -118,8 +114,8 @@ export const Editor: React.FC<Props> = observer(
}, [components]);
const appComponent = useMemo(() => {
return (
<ErrorBoundary key={recoverKey} onError={onError}>
return isDisplayApp ? (
<ErrorBoundary>
<App
options={app}
debugEvent={false}
@ -127,8 +123,8 @@ export const Editor: React.FC<Props> = observer(
gridCallbacks={gridCallbacks}
/>
</ErrorBoundary>
);
}, [App, app, gridCallbacks, recoverKey]);
) : null;
}, [App, app, gridCallbacks, isDisplayApp]);
const dataSourceForm = useMemo(() => {
let component: React.ReactNode = <ComponentForm services={services} />;
@ -148,16 +144,29 @@ export const Editor: React.FC<Props> = observer(
return component;
}, [activeDataSource, services, activeDataSourceType]);
useEffect(() => {
// when errors happened, `ErrorBoundary` wouldn't update until rerender
// so after the errors are fixed, would trigger this effect before `setError(false)`
// the process to handle the error is:
// app change -> error happen -> setError(true) -> setRecoverKey(recoverKey + 1) -> app change -> setRecoverKey(recoverKey + 1) -> setError(false)
if (isError) {
setRecoverKey(recoverKey + 1);
const onRefresh = useCallback(()=> {
services.stateManager.clear();
setIsDisplayApp(false);
onRefreshApp();
}, [services.stateManager, onRefreshApp]);
useEffect(()=> {
// Wait until the app is completely unmounted before remounting it
if (isDisplayApp === false) {
setIsDisplayApp(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [app, isError]); // it only should depend on the app schema and `isError` to update
}, [isDisplayApp]);
const onCodeMode = useCallback(v => {
setCodeMode(v);
if (!v && code) {
eventBus.send(
'operation',
genOperation(registry, 'replaceApp', {
app: new AppModel(JSON.parse(code).spec.components, registry),
})
);
}
}, [code, eventBus, registry]);
const onPreview = useCallback(() => setPreview(true), []);
const renderMain = () => {
const appBox = (
@ -324,19 +333,10 @@ export const Editor: React.FC<Props> = observer(
<EditorHeader
scale={scale}
setScale={setScale}
onPreview={() => setPreview(true)}
onPreview={onPreview}
codeMode={codeMode}
onCodeMode={v => {
setCodeMode(v);
if (!v && code) {
eventBus.send(
'operation',
genOperation(registry, 'replaceApp', {
app: new AppModel(JSON.parse(code).spec.components, registry),
})
);
}
}}
onRefresh={onRefresh}
onCodeMode={onCodeMode}
/>
<Box display="flex" flex="1" overflow="auto">
{renderMain()}

View File

@ -7,7 +7,8 @@ export const EditorHeader: React.FC<{
onPreview: () => void;
codeMode: boolean;
onCodeMode: (v: boolean) => void;
}> = ({ scale, setScale, onPreview, onCodeMode, codeMode }) => {
onRefresh: () => void;
}> = ({ scale, setScale, onPreview, onCodeMode, onRefresh, codeMode }) => {
return (
<Flex p={2} borderBottomWidth="2px" borderColor="gray.200" align="center">
<Flex flex="1">
@ -31,6 +32,9 @@ export const EditorHeader: React.FC<{
</Button>
</Flex>
<Flex flex="1" justify="end">
<Button colorScheme="blue" marginRight="8px" onClick={onRefresh}>
refresh
</Button>
<Button colorScheme="blue" onClick={onPreview}>
preview
</Button>

View File

@ -2,18 +2,15 @@ import React from 'react';
type Props = {
onError?: (error: Error | null) => void;
}
};
class ErrorBoundary extends React.Component<
Props,
{ error: unknown }
> {
class ErrorBoundary extends React.Component<Props, { error: Error | null }> {
constructor(props: Props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error: unknown) {
static getDerivedStateFromError(error: Error) {
return { error };
}
@ -27,7 +24,7 @@ class ErrorBoundary extends React.Component<
render() {
if (this.state.error) {
return String(this.state.error);
return <div style={{ whiteSpace: 'pre-line' }}>{this.state.error.stack}</div>;
}
return this.props.children;

View File

@ -1,7 +1,7 @@
import { Editor as _Editor } from './components/Editor';
import { initSunmaoUI, SunmaoLib, SunmaoUIRuntimeProps } from '@sunmao-ui/runtime';
import { AppModelManager } from './operations/AppModelManager';
import React from 'react';
import React, { useState, useCallback } from 'react';
import {
widgets as internalWidgets,
WidgetManager,
@ -96,15 +96,21 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
};
const Editor: React.FC = () => {
const [store, setStore] = useState(stateManager.store);
const onRefresh = useCallback(()=> {
setStore(stateManager.store);
}, []);
return (
<ChakraProvider theme={editorTheme}>
<_Editor
App={App}
eleMap={ui.eleMap}
registry={registry}
stateStore={stateManager.store}
stateStore={store}
services={services}
libs={props.libs || []}
onRefresh={onRefresh}
uiProps={props.uiProps||{}}
/>
</ChakraProvider>

View File

@ -1,6 +1,6 @@
{
"name": "@sunmao-ui/runtime",
"version": "0.5.6",
"version": "0.5.7",
"description": "sunmao-ui runtime",
"author": "sunmao-ui developers",
"homepage": "https://github.com/webzard-io/sunmao-ui#readme",
@ -31,7 +31,7 @@
"dependencies": {
"@emotion/css": "^11.7.1",
"@sinclair/typebox": "^0.21.2",
"@sunmao-ui/core": "^0.5.4",
"@sunmao-ui/core": "^0.5.5",
"@sunmao-ui/shared": "^0.0.0",
"@vue/reactivity": "^3.1.5",
"@vue/shared": "^3.2.20",

View File

@ -1,252 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { merge } from 'lodash-es';
import { RuntimeComponentSchema, RuntimeTraitSchema } from '@sunmao-ui/core';
import { watch } from '../../utils/watchReactivity';
import { ImplWrapperProps, TraitResult } from '../../types';
import { shallowCompareArray, CORE_VERSION, SLOT_TRAIT_NAME } from '@sunmao-ui/shared';
const _ImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>((props, ref) => {
const { component: c, app, children, services, childrenMap, hooks, isInModule } = props;
const { registry, stateManager, globalHandlerMap, apiService, eleMap } = props.services;
const childrenCache = new Map<RuntimeComponentSchema, 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)!);
const eleRef = useRef<HTMLElement>();
const onRef = (ele: HTMLElement) => {
// If a component is in module, it should not have mask, so we needn't set it
if (!isInModule) {
eleMap.set(c.id, ele);
}
hooks?.didDomUpdate && hooks?.didDomUpdate();
};
useEffect(() => {
// If a component is in module, it should not have mask, so we needn't set it
if (eleRef.current && !isInModule) {
eleMap.set(c.id, eleRef.current);
}
return () => {
if (!isInModule) {
eleMap.delete(c.id);
}
};
}, [c.id, eleMap, isInModule]);
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.current, ...map };
globalHandlerMap.set(c.id, handlerMap.current);
},
[c.id, globalHandlerMap]
);
const executeTrait = useCallback(
(trait: RuntimeTraitSchema, traitProperty: RuntimeTraitSchema['properties']) => {
const tImpl = registry.getTrait(
trait.parsedType.version,
trait.parsedType.name
).impl;
return tImpl({
...traitProperty,
trait,
componentId: c.id,
mergeState,
subscribeMethods,
services,
});
},
[c.id, mergeState, registry, services, subscribeMethods]
);
// result returned from traits
const [traitResults, setTraitResults] = useState<TraitResult<string, string>[]>([]);
// eval traits' properties then execute traits
useEffect(() => {
const stops: ReturnType<typeof watch>[] = [];
const properties: Array<RuntimeTraitSchema['properties']> = [];
c.traits.forEach((t, i) => {
const { result, stop } = stateManager.deepEvalAndWatch(
t.properties,
({ result: property }: any) => {
const traitResult = executeTrait(t, property);
setTraitResults(oldResults => {
// assume traits number and order will not change
const newResults = [...oldResults];
newResults[i] = traitResult;
return newResults;
});
stops.push(stop);
},
{ fallbackWhenError: () => undefined }
);
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) => executeTrait(trait, properties[i])));
return () => stops.forEach(s => s());
}, [c.traits, executeTrait, 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, { fallbackWhenError: () => undefined }),
propsFromTraits
);
});
// eval component properties
useEffect(() => {
const { result, stop } = stateManager.deepEvalAndWatch(
c.properties,
({ result: newResult }: any) => {
setEvaledComponentProperties({ ...newResult });
},
{ fallbackWhenError: () => undefined }
);
// must keep this line, reason is the same as above
setEvaledComponentProperties({ ...result });
return stop;
}, [c.properties, stateManager]);
useEffect(() => {
if (unmount) {
delete stateManager.store[c.id];
}
return () => {
delete stateManager.store[c.id];
};
}, [c.id, stateManager.store, unmount]);
const mergedProps = useMemo(
() => ({ ...evaledComponentProperties, ...propsFromTraits }),
[evaledComponentProperties, propsFromTraits]
);
function genSlotsElements() {
if (!childrenMap[c.id]) {
return {};
}
const res: Record<string, React.ReactElement[] | React.ReactElement> = {};
for (const slot in childrenMap[c.id]) {
const slotChildren = 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)!;
});
res[slot] = slotChildren.length === 1 ? slotChildren[0] : slotChildren;
}
return res;
}
const C = unmount ? null : (
<Impl
key={c.id}
{...props}
{...mergedProps}
slotsElements={genSlotsElements()}
mergeState={mergeState}
subscribeMethods={subscribeMethods}
elementRef={eleRef}
getElement={onRef}
/>
);
let result = (
<React.Fragment key={c.id}>
{C}
{children}
</React.Fragment>
);
let parentComponent;
const slotTrait = c.traits.find(t => t.type === `${CORE_VERSION}/${SLOT_TRAIT_NAME}`);
if (slotTrait && app) {
parentComponent = app.spec.components.find(
c => c.id === (slotTrait.properties.container as any).id
);
}
if (parentComponent?.parsedType.name === 'grid_layout') {
/* eslint-disable */
const { component, services, app, 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;
const prevComponent = prevProps.component;
const nextComponent = nextProps.component;
let isEqual = false;
if (prevChildren && nextChildren) {
isEqual = shallowCompareArray(prevChildren, nextChildren);
}
return isEqual && prevComponent === nextComponent;
}
);

View File

@ -0,0 +1,20 @@
import React from 'react';
import { ImplWrapperProps } from '../../../types';
import { shallowCompareArray } from '@sunmao-ui/shared';
import { ImplWrapperMain } from './ImplWrapperMain';
export const ImplWrapper = React.memo<ImplWrapperProps>(
ImplWrapperMain,
(prevProps, nextProps) => {
const prevChildren = prevProps.childrenMap[prevProps.component.id]?._grandChildren;
const nextChildren = nextProps.childrenMap[nextProps.component.id]?._grandChildren;
const prevComponent = prevProps.component;
const nextComponent = nextProps.component;
let isEqual = false;
if (prevChildren && nextChildren) {
isEqual = shallowCompareArray(prevChildren, nextChildren);
}
return isEqual && prevComponent === nextComponent;
}
);

View File

@ -0,0 +1,177 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { merge, mergeWith, isArray } from 'lodash-es';
import { RuntimeTraitSchema } from '@sunmao-ui/core';
import { watch } from '../../../utils/watchReactivity';
import { ImplWrapperProps, TraitResult } from '../../../types';
import { useRuntimeFunctions } from './hooks/useRuntimeFunctions';
import { useSlotElements } from './hooks/useSlotChildren';
import { useGlobalHandlerMap } from './hooks/useGlobalHandlerMap';
import { useEleRef } from './hooks/useEleMap';
import { useGridLayout } from './hooks/useGridLayout';
export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
(props, ref) => {
const { component: c, children } = props;
const { registry, stateManager } = props.services;
const Impl = registry.getComponent(c.parsedType.version, c.parsedType.name).impl;
useGlobalHandlerMap(props);
const { eleRef, onRef } = useEleRef(props);
const { mergeState, subscribeMethods, executeTrait } = useRuntimeFunctions(props);
const [traitResults, setTraitResults] = useState<TraitResult<string, string>[]>(
() => {
return c.traits.map(t => executeTrait(t, stateManager.deepEval(t.properties)));
}
);
useEffect(() => {
return () => {
delete stateManager.store[c.id];
};
}, [c.id, stateManager.store]);
// eval traits' properties then execute traits
useEffect(() => {
const stops: ReturnType<typeof watch>[] = [];
const properties: Array<RuntimeTraitSchema['properties']> = [];
c.traits.forEach((t, i) => {
const { result, stop } = stateManager.deepEvalAndWatch(
t.properties,
({ result: property }: any) => {
const traitResult = executeTrait(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) => executeTrait(trait, properties[i])));
return () => stops.forEach(s => s());
}, [c.id, c.traits, executeTrait, 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;
}
return mergeWith(prevProps, result.props, (obj, src) => {
if (isArray(obj)) {
return obj.concat(src);
}
});
},
{} as TraitResult<string, string>['props']
);
}, [traitResults]);
// component properties
const [evaledComponentProperties, setEvaledComponentProperties] = useState(() => {
return merge(
stateManager.deepEval(c.properties, { fallbackWhenError: () => undefined }),
propsFromTraits
);
});
// eval component properties
useEffect(() => {
const { result, stop } = stateManager.deepEvalAndWatch(
c.properties,
({ result: newResult }: any) => {
setEvaledComponentProperties({ ...newResult });
},
{ fallbackWhenError: () => undefined }
);
// must keep this line, reason is the same as above
setEvaledComponentProperties({ ...result });
return stop;
}, [c.properties, stateManager]);
useEffect(() => {
const clearFunctions = propsFromTraits?.componentDidMount?.map(e => e());
return () => {
clearFunctions?.forEach(func => func && func());
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useDidUpdate(() => {
const clearFunctions = propsFromTraits?.componentDidUpdate?.map(e => e());
return () => {
clearFunctions?.forEach(func => func && func());
};
});
useDidUnmount(() => {
propsFromTraits?.componentDidUnmount?.forEach(e => e());
});
const mergedProps = useMemo(
() => ({ ...evaledComponentProperties, ...propsFromTraits }),
[evaledComponentProperties, propsFromTraits]
);
const unmount = traitResults.some(result => result.unmount);
const slotElements = useSlotElements(props);
const C = unmount ? null : (
<Impl
key={c.id}
{...props}
{...mergedProps}
slotsElements={slotElements}
mergeState={mergeState}
subscribeMethods={subscribeMethods}
elementRef={eleRef}
getElement={onRef}
/>
);
const result = (
<React.Fragment key={c.id}>
{C}
{children}
</React.Fragment>
);
const element = useGridLayout(props, result, ref);
return element;
}
);
// This hook will only run unmount function when unmount, not every time when unmount function changes.
const useDidUnmount = (fn: Function) => {
const fnRef = useRef(fn);
fnRef.current = fn;
useEffect(() => () => fnRef.current(), []);
};
// This hook will run every times when component update, except when first render.
const useDidUpdate = (fn: Function) => {
const fnRef = useRef(fn);
const hasMounted = useRef(false);
fnRef.current = fn;
useEffect(() => {
if (hasMounted.current) {
return fnRef.current();
} else {
hasMounted.current = true;
}
});
};

View File

@ -0,0 +1,35 @@
import { useEffect, useRef } from 'react';
import { ImplWrapperProps } from '../../../../types';
export function useEleRef(props: ImplWrapperProps) {
const { component: c, services, hooks, isInModule } = props;
const { stateManager, eleMap } = services;
const eleRef = useRef<HTMLElement>();
const onRef = (ele: HTMLElement) => {
// If a component is in module, it should not have mask, so we needn't set it
if (!isInModule) {
eleMap.set(c.id, ele);
}
hooks?.didDomUpdate && hooks?.didDomUpdate();
};
useEffect(() => {
// If a component is in module, it should not have mask, so we needn't set it
if (eleRef.current && !isInModule) {
eleMap.set(c.id, eleRef.current);
}
return () => {
if (!isInModule) {
eleMap.delete(c.id);
}
};
// These dependencies should not change in the whole life cycle of ImplWrapper.
// Otherwise, the clear function will run unexpectedly
}, [c.id, eleMap, isInModule, stateManager.store]);
return {
eleRef,
onRef,
};
}

View File

@ -0,0 +1,34 @@
import { useEffect } from 'react';
import { ImplWrapperProps } from '../../../../types';
export function useGlobalHandlerMap(props: ImplWrapperProps) {
const { component: c, services } = props;
const { apiService, globalHandlerMap } = services;
useEffect(() => {
if (!globalHandlerMap.has(c.id)) {
globalHandlerMap.set(c.id, {});
}
const handlerMap = globalHandlerMap.get(c.id);
if (!handlerMap) return;
const handler = (s: { componentId: string; name: string; parameters?: any }) => {
if (s.componentId !== c.id) {
return;
}
if (!handlerMap[s.name]) {
// maybe log?
return;
}
handlerMap[s.name](s.parameters);
};
apiService.on('uiMethod', handler);
return () => {
apiService.off('uiMethod', handler);
handlerMap.delete(c.id);
};
}, [apiService, c.id, globalHandlerMap]);
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { ImplWrapperProps } from '../../../../types';
import { CORE_VERSION, SLOT_TRAIT_NAME, GRID_LAYOUT_COMPONENT_NAME } from '@sunmao-ui/shared';
export function useGridLayout(
props: ImplWrapperProps,
node: React.ReactNode,
ref: React.ForwardedRef<HTMLDivElement>
) {
const { component: c, app } = props;
let parentComponent;
const slotTrait = c.traits.find(t => t.type === `${CORE_VERSION}/${SLOT_TRAIT_NAME}`);
if (slotTrait && app) {
parentComponent = app.spec.components.find(
c => c.id === (slotTrait.properties.container as any).id
);
}
if (parentComponent?.parsedType.name === GRID_LAYOUT_COMPONENT_NAME) {
/* eslint-disable */
const { component, services, app, gridCallbacks, ...restProps } = props;
/* eslint-enable */
return (
<div key={c.id} data-sunmao-ui-id={c.id} ref={ref} {...restProps}>
{node}
</div>
);
}
return <>{node}</>;
}

View File

@ -0,0 +1,47 @@
import { useCallback } from 'react';
import { RuntimeTraitSchema } from '@sunmao-ui/core';
import { ImplWrapperProps } from '../../../../types';
import { merge } from 'lodash';
import { HandlerMap } from '../../../../services/handler';
export function useRuntimeFunctions(props: ImplWrapperProps) {
const { component: c, services } = props;
const { stateManager, registry, globalHandlerMap } = services;
const mergeState = useCallback(
(partial: any) => {
stateManager.store[c.id] = { ...stateManager.store[c.id], ...partial };
},
[c.id, stateManager.store]
);
const subscribeMethods = useCallback(
(map: HandlerMap) => {
const oldMap = globalHandlerMap.get(c.id);
globalHandlerMap.set(c.id, merge(oldMap, map));
},
[c.id, globalHandlerMap]
);
const executeTrait = useCallback(
(trait: RuntimeTraitSchema, traitProperty: RuntimeTraitSchema['properties']) => {
const tImpl = registry.getTrait(
trait.parsedType.version,
trait.parsedType.name
).impl;
return tImpl({
...traitProperty,
trait,
componentId: c.id,
mergeState,
subscribeMethods,
services,
});
},
[c.id, mergeState, registry, services, subscribeMethods]
);
return {
mergeState,
subscribeMethods,
executeTrait,
};
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import { RuntimeComponentSchema } from '@sunmao-ui/core';
import { ImplWrapperProps } from '../../../../types';
import { ImplWrapper } from '../ImplWrapper';
export function useSlotElements(props: ImplWrapperProps) {
const { component: c, childrenMap } = props;
const childrenCache = new Map<RuntimeComponentSchema, React.ReactElement>();
if (!childrenMap[c.id]) {
return {};
}
const slotElements: Record<string, React.ReactElement[] | React.ReactElement> = {};
for (const slot in childrenMap[c.id]) {
const slotChildren = 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)!;
});
slotElements[slot] = slotChildren.length === 1 ? slotChildren[0] : slotChildren;
}
return slotElements;
}

View File

@ -0,0 +1 @@
export * from './ImplWrapper';

View File

@ -1,5 +1,4 @@
import { implementRuntimeComponent } from '../../utils/buildKit';
import { useEffect } from 'react';
import { Type } from '@sinclair/typebox';
import { CORE_VERSION, DUMMY_COMPONENT_NAME } from '@sunmao-ui/shared';
@ -25,12 +24,6 @@ export default implementRuntimeComponent({
styleSlots: [],
events: [],
},
})(({ effects }) => {
useEffect(() => {
return () => {
effects?.forEach(e => e());
};
}, [effects]);
})(() => {
return null;
});

View File

@ -1,4 +1,4 @@
type HandlerMap = Record<string, (parameters?: any) => void>;
export type HandlerMap = Record<string, (parameters?: any) => void>;
export type GlobalHandlerMap = Map<string, HandlerMap>;

View File

@ -221,8 +221,8 @@ export default implementRuntimeTrait({
},
});
return {
props: null,
return {
props: {},
};
};
};
});

View File

@ -39,8 +39,6 @@ export default implementRuntimeTrait({
const hasInitialized = HasInitializedMap.get(hashId);
if (!hasInitialized) {
mergeState({ [key]: initialValue });
const methods = {
setValue({ key, value }: KeyValue) {
mergeState({ [key]: value });
@ -54,7 +52,18 @@ export default implementRuntimeTrait({
}
return {
props: null,
props: {
componentDidMount: [
() => {
mergeState({ [key]: initialValue });
},
],
componentDidUnmount: [
() => {
HasInitializedMap.delete(hashId);
},
],
},
};
};
});

View File

@ -7,7 +7,9 @@ export type TraitResult<KStyleSlot extends string, KEvent extends string> = {
data?: unknown;
customStyle?: Record<KStyleSlot, string>;
callbackMap?: CallbackMap<KEvent>;
effects?: Array<() => void>;
componentDidUnmount?: Array<() => void>;
componentDidMount?: Array<() => Function | void>
componentDidUpdate?: Array<() => Function | void>
} | null;
unmount?: boolean;
};