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

This commit is contained in:
MrWindlike 2022-04-26 14:09:17 +08:00
commit f304529899
12 changed files with 142 additions and 49 deletions

View File

@ -10,7 +10,7 @@ describe('trait', () => {
},
spec: {
properties: [{ name: 'width', type: 'number' }],
properties: { name: 'width', type: 'number' },
state: {
type: 'string',
},
@ -29,6 +29,7 @@ describe('trait', () => {
Object {
"kind": "Trait",
"metadata": Object {
"annotations": Object {},
"description": "",
"name": "test_trait",
},
@ -45,12 +46,10 @@ describe('trait', () => {
},
},
],
"properties": Array [
Object {
"name": "width",
"type": "number",
},
],
"properties": Object {
"name": "width",
"type": "number",
},
"state": Object {
"type": "string",
},

View File

@ -5,10 +5,12 @@ import { parseVersion, Version } from './version';
// spec
type TraitMetaData = Metadata<{ beforeRender?: boolean }>;
export type Trait = {
version: string;
kind: 'Trait';
metadata: Metadata;
metadata: TraitMetaData;
spec: TraitSpec;
};
@ -26,7 +28,7 @@ export type RuntimeTrait = Trait & {
// partial some fields, use as param createModule
export type CreateTraitOptions = {
version: string;
metadata: Metadata;
metadata: TraitMetaData;
spec?: Partial<TraitSpec>;
};
@ -39,6 +41,7 @@ export function createTrait(options: CreateTraitOptions): RuntimeTrait {
metadata: {
name: options.metadata.name,
description: options.metadata.description || '',
annotations: options.metadata.annotations || {},
},
spec: {
properties: {},

View File

@ -117,6 +117,14 @@ export const EditorMask: React.FC<Props> = observer((props: Props) => {
[resizeObserver]
);
// because this useEffect would run after sunmao didMount hook, so it cannot subscribe the first HTMLElementsUpdated event
// we should call the callback function after first render
useEffect(() => {
observeResize(eleMap);
updateCoordinateSystem(eleMap);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
eventBus.on('HTMLElementsUpdated', () => {
observeResize(eleMap);

View File

@ -51,7 +51,6 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
);
const didMount = () => {
editorStore.eleMap = ui.eleMap;
eventBus.send('HTMLElementsUpdated');
};
const didUpdate = () => {
@ -61,7 +60,10 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
eventBus.send('HTMLElementsUpdated');
};
const ui = initSunmaoUI({ ...props.runtimeProps, hooks: { didMount, didUpdate, didDomUpdate } });
const ui = initSunmaoUI({
...props.runtimeProps,
hooks: { didMount, didUpdate, didDomUpdate },
});
const App = ui.App;
const registry = ui.registry;
@ -84,6 +86,8 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
);
const widgetManager = new WidgetManager();
const editorStore = new EditorStore(eventBus, registry, stateManager, appStorage);
editorStore.eleMap = ui.eleMap;
const services = {
App,
registry: ui.registry,
@ -97,7 +101,7 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
const Editor: React.FC = () => {
const [store, setStore] = useState(stateManager.store);
const onRefresh = useCallback(()=> {
const onRefresh = useCallback(() => {
setStore(stateManager.store);
}, []);
@ -111,7 +115,7 @@ export function initSunmaoUIEditor(props: SunmaoUIEditorProps = {}) {
services={services}
libs={props.libs || []}
onRefresh={onRefresh}
uiProps={props.uiProps||{}}
uiProps={props.uiProps || {}}
/>
</ChakraProvider>
);

View File

@ -28,7 +28,7 @@ describe('evalExpression function', () => {
};
const stateManager = new StateManager();
it('can eval {{}} expression', () => {
const evalOptions = { evalListItem: false, scopeObject: scope };
const evalOptions = { evalListItem: false, scopeObject: scope, noConsoleError: true };
expect(stateManager.maskedEval('value', evalOptions)).toEqual('value');
expect(stateManager.maskedEval('{{true}}', evalOptions)).toEqual(true);
@ -83,6 +83,7 @@ describe('evalExpression function', () => {
stateManager.maskedEval('{{value}}', {
scopeObject: { override: 'foo' },
overrideScope: true,
noConsoleError: true,
})
).toBeInstanceOf(ExpressionError);
expect(
@ -97,6 +98,7 @@ describe('evalExpression function', () => {
expect(
stateManager.maskedEval('{{wrongExp}}', {
fallbackWhenError: exp => exp,
noConsoleError: true,
})
).toEqual('{{wrongExp}}');
});

View File

@ -1,10 +1,10 @@
import React from 'react';
import { ImplWrapperProps } from '../../../types';
import { shallowCompareArray } from '@sunmao-ui/shared';
import { ImplWrapperMain } from './ImplWrapperMain';
import { UnmountImplWrapper } from './UnmountImplWrapper';
export const ImplWrapper = React.memo<ImplWrapperProps>(
ImplWrapperMain,
UnmountImplWrapper,
(prevProps, nextProps) => {
const prevChildren = prevProps.childrenMap[prevProps.component.id]?._grandChildren;
const nextChildren = nextProps.childrenMap[nextProps.component.id]?._grandChildren;

View File

@ -49,9 +49,9 @@ export const ImplWrapperMain = React.forwardRef<HTMLDivElement, ImplWrapperProps
newResults[i] = traitResult;
return newResults;
});
stops.push(stop);
}
);
stops.push(stop);
properties.push(result);
});
// although traitResults has initialized in useState, it must be set here again

View File

@ -0,0 +1,65 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { RuntimeTraitSchema } from '@sunmao-ui/core';
import { ImplWrapperMain } from './ImplWrapperMain';
import { useRuntimeFunctions } from './hooks/useRuntimeFunctions';
import { initSingleComponentState } from '../../../utils/initStateAndMethod';
import { ImplWrapperProps, TraitResult } from '../../../types';
import { watch } from '../../..';
export const UnmountImplWrapper = React.forwardRef<HTMLDivElement, ImplWrapperProps>(
(props, ref) => {
const { component: c, services } = props;
const { stateManager, registry } = services;
const { executeTrait } = useRuntimeFunctions(props);
const unmountTraits = useMemo(
() => c.traits.filter(t => registry.getTraitByType(t.type).metadata.annotations?.beforeRender),
[c.traits, registry]
);
const [isHidden, setIsHidden] = useState(() => {
const results: TraitResult<string, string>[] = unmountTraits.map(t => {
const properties = stateManager.deepEval(t.properties);
return executeTrait(t, properties);
});
return results.some(result => result.unmount);
});
const traitChangeCallback = useCallback(
(trait: RuntimeTraitSchema, properties: Record<string, unknown>) => {
const result = executeTrait(trait, properties);
setIsHidden(!!result.unmount);
if (result.unmount) {
// Every component's state is initialized in initStateAnd Method.
// So if if it should not render, we should remove it from store.
delete stateManager.store[c.id];
}
},
[c.id, executeTrait, stateManager]
);
useEffect(() => {
const stops: ReturnType<typeof watch>[] = [];
if (unmountTraits.length > 0) {
unmountTraits.forEach(t => {
const { result, stop } = stateManager.deepEvalAndWatch(t.properties, newValue =>
traitChangeCallback(t, newValue.result)
);
traitChangeCallback(t, result);
stops.push(stop);
});
}
return () => {
stops.forEach(stop => stop());
};
}, [c, executeTrait, unmountTraits, stateManager, traitChangeCallback]);
// If a component is unmount, its state would be removed.
// So if it mount again, we should init its state again.
if (!isHidden && !stateManager.store[c.id]) {
initSingleComponentState(registry, stateManager, c);
}
return !isHidden ? <ImplWrapperMain {...props} ref={ref} /> : null;
}
);

View File

@ -28,7 +28,7 @@ export function useGlobalHandlerMap(props: ImplWrapperProps) {
return () => {
apiService.off('uiMethod', handler);
handlerMap.delete(c.id);
globalHandlerMap.delete(c.id);
};
}, [apiService, c.id, globalHandlerMap]);
}

View File

@ -19,6 +19,7 @@ type EvalOptions = {
scopeObject?: Record<string, any>;
overrideScope?: boolean;
fallbackWhenError?: (exp: string) => any;
noConsoleError?: boolean
};
// TODO: use web worker
@ -76,7 +77,7 @@ export class StateManager {
};
maskedEval(raw: string, options: EvalOptions = {}): unknown | ExpressionError {
const { evalListItem = false, fallbackWhenError } = options;
const { evalListItem = false, fallbackWhenError, noConsoleError } = options;
let result: unknown[] = [];
try {
@ -104,8 +105,10 @@ export class StateManager {
} catch (error) {
if (error instanceof Error) {
const expressionError = new ExpressionError(error.message);
consoleError(ConsoleType.Expression, '', expressionError.message);
if (!noConsoleError) {
consoleError(ConsoleType.Expression, '', expressionError.message);
}
return fallbackWhenError ? fallbackWhenError(raw) : expressionError;
}

View File

@ -12,6 +12,9 @@ export default implementRuntimeTrait({
metadata: {
name: HIDDEN_TRAIT_NAME,
description: 'render component with condition',
annotations: {
beforeRender: true,
},
},
spec: {
properties: HiddenTraitPropertiesSpec,

View File

@ -1,4 +1,4 @@
import { RuntimeApplication } from '@sunmao-ui/core';
import { RuntimeComponentSchema } from '@sunmao-ui/core';
import { Static, TSchema } from '@sinclair/typebox';
import { RegistryInterface } from '../services/Registry';
import { StateManagerInterface } from '../services/StateManager';
@ -12,32 +12,38 @@ import {
export function initStateAndMethod(
registry: RegistryInterface,
stateManager: StateManagerInterface,
components: RuntimeApplication['spec']['components']
components: RuntimeComponentSchema[]
) {
components.forEach(c => {
if (stateManager.store[c.id]) {
return false;
}
let state = {};
c.traits.forEach(t => {
const tSpec = registry.getTrait(t.parsedType.version, t.parsedType.name).spec;
state = { ...state, ...parseTypeBox(tSpec.state as TSchema) };
});
const cSpec = registry.getComponent(c.parsedType.version, c.parsedType.name).spec;
state = { ...state, ...parseTypeBox(cSpec.state as TSchema) };
stateManager.store[c.id] = state;
if (c.type === `${CORE_VERSION}/${MODULE_CONTAINER_COMPONENT_NAME}`) {
const moduleSchema = c.properties as Static<typeof ModuleSpec>;
try {
const mSpec = registry.getModuleByType(moduleSchema.type).spec;
const moduleInitState: Record<string, unknown> = {};
for (const key in mSpec) {
moduleInitState[key] = undefined;
}
stateManager.store[moduleSchema.id] = moduleInitState;
} catch {}
}
});
components.forEach(c => initSingleComponentState(registry, stateManager, c));
}
export function initSingleComponentState(
registry: RegistryInterface,
stateManager: StateManagerInterface,
c: RuntimeComponentSchema
) {
if (stateManager.store[c.id]) {
return false;
}
let state = {};
c.traits.forEach(t => {
const tSpec = registry.getTrait(t.parsedType.version, t.parsedType.name).spec;
state = { ...state, ...parseTypeBox(tSpec.state as TSchema) };
});
const cSpec = registry.getComponent(c.parsedType.version, c.parsedType.name).spec;
state = { ...state, ...parseTypeBox(cSpec.state as TSchema) };
stateManager.store[c.id] = state;
if (c.type === `${CORE_VERSION}/${MODULE_CONTAINER_COMPONENT_NAME}`) {
const moduleSchema = c.properties as Static<typeof ModuleSpec>;
try {
const mSpec = registry.getModuleByType(moduleSchema.type).spec;
const moduleInitState: Record<string, unknown> = {};
for (const key in mSpec) {
moduleInitState[key] = undefined;
}
stateManager.store[moduleSchema.id] = moduleInitState;
} catch {}
}
}