mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-02-17 17:40:31 +08:00
Merge branch 'main' of https://github.com/webzard-io/sunmao-ui into feat/shared
This commit is contained in:
commit
bc2b9ae154
@ -38,6 +38,7 @@
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"body": "{{ form.data }}",
|
||||
"bodyType": "json",
|
||||
"onComplete": [
|
||||
{
|
||||
"componentId": "$utils",
|
||||
|
@ -38,6 +38,7 @@
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"body": "{{ form.data }}",
|
||||
"bodyType": "json",
|
||||
"onComplete": [
|
||||
{
|
||||
"componentId": "$utils",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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()}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
@ -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;
|
||||
}
|
||||
);
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
@ -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,
|
||||
};
|
||||
}
|
@ -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]);
|
||||
}
|
@ -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}</>;
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './ImplWrapper';
|
@ -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;
|
||||
});
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -221,8 +221,8 @@ export default implementRuntimeTrait({
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: null,
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -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);
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user