mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-01-30 17:09:35 +08:00
Merge pull request #53 from webzard-io/feat/list
implement List component
This commit is contained in:
commit
646fdf2c89
@ -1,12 +1,4 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
".ts": "ts-jest",
|
||||
},
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
diagnostics: false,
|
||||
},
|
||||
},
|
||||
moduleFileExtensions: ["ts", "js"],
|
||||
testMatch: ["<rootDir>/__tests__/**/**.spec.ts"],
|
||||
testPathIgnorePatterns: ["/node_modules/", "/lib/", "<rootDir>/lib/"],
|
||||
|
7
packages/core/babel.config.js
Normal file
7
packages/core/babel.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
// only for jest
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
};
|
@ -36,7 +36,6 @@
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"jest": "^27.0.6",
|
||||
"prettier": "^2.3.2",
|
||||
"ts-jest": "^27.0.3",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
|
35
packages/runtime/__tests__/expression.spec.ts
Normal file
35
packages/runtime/__tests__/expression.spec.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { parseExpression } from '../src/store';
|
||||
|
||||
describe('parseExpression function', () => {
|
||||
it('can parse {{}} expression', () => {
|
||||
expect(parseExpression('{{ value }}')).toMatchObject([
|
||||
{ expression: 'value', isDynamic: true },
|
||||
]);
|
||||
expect(parseExpression('Hello, {{ value }}!')).toMatchObject([
|
||||
{ expression: 'Hello, ', isDynamic: false },
|
||||
{ expression: 'value', isDynamic: true },
|
||||
{ expression: '!', isDynamic: false },
|
||||
]);
|
||||
expect(
|
||||
parseExpression('{{ $listItem.name }} is in {{ root.listTitle }} list')
|
||||
).toMatchObject([
|
||||
{ expression: '{{$listItem.name}}', isDynamic: false },
|
||||
{ expression: ' is in ', isDynamic: false },
|
||||
{ expression: 'root.listTitle', isDynamic: true },
|
||||
{ expression: ' list', isDynamic: false },
|
||||
]);
|
||||
expect(parseExpression('{{{{}}}}}}')).toMatchObject([
|
||||
{ expression: '{{', isDynamic: true },
|
||||
{ expression: '}}}}', isDynamic: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('can parse $listItem expression', () => {
|
||||
expect(parseExpression('{{ $listItem.value }}')).toMatchObject([
|
||||
{ expression: '{{$listItem.value}}', isDynamic: false },
|
||||
]);
|
||||
expect(parseExpression('{{ $listItem.value }}', true)).toMatchObject([
|
||||
{ expression: '$listItem.value', isDynamic: true },
|
||||
]);
|
||||
});
|
||||
});
|
7
packages/runtime/babel.config.js
Normal file
7
packages/runtime/babel.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
// only for jest
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
};
|
476
packages/runtime/example/list/list.html
Normal file
476
packages/runtime/example/list/list.html
Normal file
@ -0,0 +1,476 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>meta-ui runtime example: list component</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module">
|
||||
import renderApp from '../../src/main.tsx';
|
||||
const listdata = [
|
||||
{
|
||||
id: 1,
|
||||
name: '马云',
|
||||
email: 'jack.ma@deck.com',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '马化腾',
|
||||
email: 'pony.ma@conversation.com',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '李彦宏',
|
||||
email: 'robin.li@response.com',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '张一鸣',
|
||||
email: 'yiming.zhang@example.com',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '王兴',
|
||||
email: 'xing.wang@widget.org',
|
||||
},
|
||||
];
|
||||
|
||||
renderApp({
|
||||
version: 'example/v1',
|
||||
metadata: {
|
||||
name: 'list_component',
|
||||
description: 'list component example',
|
||||
},
|
||||
spec: {
|
||||
components: [
|
||||
{
|
||||
id: 'root',
|
||||
type: 'chakra_ui/v1/root',
|
||||
properties: {},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/state',
|
||||
properties: {
|
||||
key: 'listTitle',
|
||||
initialValue: '客户列表',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'core/v1/arrayState',
|
||||
properties: {
|
||||
key: 'listData',
|
||||
initialValue: listdata,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'title',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: '**{{root.listTitle}}**',
|
||||
format: 'md',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'root',
|
||||
slot: 'root',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
type: 'chakra_ui/v1/list',
|
||||
properties: {
|
||||
listData: '{{ root.listData }}',
|
||||
template: [
|
||||
{
|
||||
id: 'listItemTemplate-{{$listItem.id}}',
|
||||
type: 'chakra_ui/v1/hstack',
|
||||
properties: {
|
||||
spacing: '24px',
|
||||
},
|
||||
traits: [],
|
||||
},
|
||||
{
|
||||
id: 'listItemName-{{$listItem.id}}',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: '姓名:{{$listItem.name}}',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'listItemTemplate-{{$listItem.id}}',
|
||||
slot: 'content',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'listItemEmail-{{$listItem.id}}',
|
||||
type: 'core/v1/text',
|
||||
properties: {
|
||||
value: {
|
||||
raw: 'email:{{$listItem.email}}',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'listItemTemplate-{{$listItem.id}}',
|
||||
slot: 'content',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'listItemButton-{{$listItem.id}}',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: {
|
||||
raw: '编辑',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/event',
|
||||
parsedType: {
|
||||
version: 'core/v1',
|
||||
name: 'event',
|
||||
},
|
||||
properties: {
|
||||
events: [
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'saveButton',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: {
|
||||
key: 'editingId',
|
||||
value: '{{ $listItem.id }}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'nameInput',
|
||||
method: {
|
||||
name: 'setInputValue',
|
||||
parameters: {
|
||||
value: '{{ $listItem.name }}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'emailInput',
|
||||
method: {
|
||||
name: 'setInputValue',
|
||||
parameters: {
|
||||
value: '{{ $listItem.email }}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'listItemTemplate-{{$listItem.id}}',
|
||||
slot: 'content',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'listItemDelete-{{$listItem.id}}',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: {
|
||||
raw: '删除',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/event',
|
||||
parsedType: {
|
||||
version: 'core/v1',
|
||||
name: 'event',
|
||||
},
|
||||
properties: {
|
||||
events: [
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'root',
|
||||
method: {
|
||||
name: 'deleteItemById',
|
||||
parameters: {
|
||||
key: 'listData',
|
||||
itemIdKey: 'id',
|
||||
itemId: '{{ $listItem.id }}',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'listItemTemplate-{{$listItem.id}}',
|
||||
slot: 'content',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'root',
|
||||
slot: 'root',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'nameInput',
|
||||
type: 'chakra_ui/v1/input',
|
||||
properties: {
|
||||
left: {
|
||||
type: 'addon',
|
||||
children: '姓名',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'root',
|
||||
slot: 'root',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'emailInput',
|
||||
type: 'chakra_ui/v1/input',
|
||||
properties: {
|
||||
left: {
|
||||
type: 'addon',
|
||||
children: '邮箱',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'root',
|
||||
slot: 'root',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'addButton',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: {
|
||||
raw: '添加客户',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/event',
|
||||
parsedType: {
|
||||
version: 'core/v1',
|
||||
name: 'event',
|
||||
},
|
||||
properties: {
|
||||
events: [
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'root',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: {
|
||||
key: 'listData',
|
||||
value: `{{
|
||||
(function() {
|
||||
let list = root.listData || []
|
||||
return list.concat({
|
||||
id: Date.now(),
|
||||
name: nameInput.value,
|
||||
email: emailInput.value
|
||||
})
|
||||
})()
|
||||
}}`,
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'root',
|
||||
slot: 'root',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'saveButton',
|
||||
type: 'chakra_ui/v1/button',
|
||||
properties: {
|
||||
text: {
|
||||
raw: '保存',
|
||||
format: 'plain',
|
||||
},
|
||||
},
|
||||
traits: [
|
||||
{
|
||||
type: 'core/v1/state',
|
||||
properties: {
|
||||
key: 'editingId',
|
||||
initialValue: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'core/v1/event',
|
||||
parsedType: {
|
||||
version: 'core/v1',
|
||||
name: 'event',
|
||||
},
|
||||
properties: {
|
||||
events: [
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'root',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: {
|
||||
key: 'listData',
|
||||
value: `{{
|
||||
(function() {
|
||||
const list = [...root.listData || []]
|
||||
let index = list.findIndex((item) => item.id == saveButton.editingId)
|
||||
list[index] = {
|
||||
...list[index],
|
||||
name: nameInput.value,
|
||||
email: emailInput.value
|
||||
}
|
||||
return list
|
||||
})()
|
||||
}}`,
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'saveButton',
|
||||
method: {
|
||||
name: 'setValue',
|
||||
parameters: {
|
||||
key: 'editingId',
|
||||
value: '-1',
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'nameInput',
|
||||
method: {
|
||||
name: 'setInputValue',
|
||||
parameters: {
|
||||
value: ``,
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
{
|
||||
event: 'click',
|
||||
componentId: 'emailInput',
|
||||
method: {
|
||||
name: 'setInputValue',
|
||||
parameters: {
|
||||
value: ``,
|
||||
},
|
||||
},
|
||||
wait: {},
|
||||
disabled: 'false',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'core/v1/slot',
|
||||
properties: {
|
||||
container: {
|
||||
id: 'root',
|
||||
slot: 'root',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
3
packages/runtime/jest.config.js
Normal file
3
packages/runtime/jest.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
...require("../../config/jest.config"),
|
||||
};
|
@ -2,7 +2,8 @@
|
||||
"name": "@meta-ui/runtime",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite"
|
||||
"dev": "vite",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.6.5",
|
||||
@ -28,12 +29,17 @@
|
||||
"wouter": "^2.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/preset-env": "^7.15.4",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/lodash": "^4.14.170",
|
||||
"@types/prismjs": "^1.16.6",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-grid-layout": "^1.1.2",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.1",
|
||||
"babel-jest": "^27.1.0",
|
||||
"jest": "^27.1.0",
|
||||
"typescript": "^4.3.2",
|
||||
"vite": "^2.3.8"
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ type ArrayElement<ArrayType extends readonly unknown[]> =
|
||||
type ApplicationComponent = RuntimeApplication['spec']['components'][0];
|
||||
type ApplicationTrait = ArrayElement<ApplicationComponent['traits']>;
|
||||
|
||||
const ImplWrapper = React.forwardRef<
|
||||
export const ImplWrapper = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
component: ApplicationComponent;
|
||||
@ -31,11 +31,6 @@ const ImplWrapper = React.forwardRef<
|
||||
[key: string]: any;
|
||||
}
|
||||
>(({ component: c, slotsMap, targetSlot, app, children, ...props }, ref) => {
|
||||
// TODO: find better way to add barrier
|
||||
if (!stateStore[c.id]) {
|
||||
stateStore[c.id] = {};
|
||||
}
|
||||
|
||||
const Impl = registry.getComponent(
|
||||
c.parsedType.version,
|
||||
c.parsedType.name
|
||||
@ -64,6 +59,7 @@ const ImplWrapper = React.forwardRef<
|
||||
apiService.on('uiMethod', handler);
|
||||
return () => {
|
||||
apiService.off('uiMethod', handler);
|
||||
globalHandlerMap.delete(c.id);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -145,10 +141,11 @@ const ImplWrapper = React.forwardRef<
|
||||
mergeState={mergeState}
|
||||
subscribeMethods={subscribeMethods}
|
||||
slotsMap={slotsMap}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
|
||||
if (targetSlot) {
|
||||
if (targetSlot && app) {
|
||||
const targetC = app.spec.components.find(c => c.id === targetSlot.id);
|
||||
if (targetC?.parsedType.name === 'grid_layout') {
|
||||
return (
|
||||
@ -227,14 +224,17 @@ export type SlotsMap = Map<
|
||||
}>
|
||||
>;
|
||||
|
||||
export function resolveAppComponents(app: RuntimeApplication): {
|
||||
export function resolveAppComponents(
|
||||
components: RuntimeApplication['spec']['components'],
|
||||
app?: RuntimeApplication
|
||||
): {
|
||||
topLevelComponents: RuntimeApplication['spec']['components'];
|
||||
slotComponentsMap: SlotComponentMap;
|
||||
} {
|
||||
const topLevelComponents: RuntimeApplication['spec']['components'] = [];
|
||||
const slotComponentsMap: SlotComponentMap = new Map();
|
||||
|
||||
for (const c of app.spec.components) {
|
||||
for (const c of components) {
|
||||
// handle component with slot trait
|
||||
const slotTrait = c.traits.find(t => t.parsedType.name === 'slot');
|
||||
if (slotTrait) {
|
||||
@ -283,7 +283,7 @@ const App: React.FC<{
|
||||
}> = ({ options, debugStore = true, debugEvent = true }) => {
|
||||
const app = createApplication(options);
|
||||
const { topLevelComponents, slotComponentsMap } = useMemo(
|
||||
() => resolveAppComponents(app),
|
||||
() => resolveAppComponents(app.spec.components, app),
|
||||
[app]
|
||||
);
|
||||
|
||||
|
@ -67,6 +67,7 @@ const Input: ComponentImplementation<{
|
||||
left,
|
||||
right,
|
||||
mergeState,
|
||||
subscribeMethods,
|
||||
data,
|
||||
}) => {
|
||||
const [value, setValue] = React.useState(''); // TODO: pin input
|
||||
@ -78,6 +79,14 @@ const Input: ComponentImplementation<{
|
||||
mergeState({ ...data });
|
||||
}, [value, data]);
|
||||
|
||||
useEffect(() => {
|
||||
subscribeMethods({
|
||||
setInputValue({ value }) {
|
||||
setValue(value);
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InputGroup size={size}>
|
||||
{left ? (
|
||||
@ -94,6 +103,7 @@ const Input: ComponentImplementation<{
|
||||
<></>
|
||||
)}
|
||||
<BaseInput
|
||||
value={value}
|
||||
variant={variant}
|
||||
placeholder={placeholder}
|
||||
focusBorderColor={focusBorderColor}
|
||||
@ -166,7 +176,14 @@ export default {
|
||||
],
|
||||
acceptTraits: [],
|
||||
state: StateSchema,
|
||||
methods: [],
|
||||
methods: [
|
||||
{
|
||||
name: 'setInputValue',
|
||||
parameters: Type.Object({
|
||||
value: Type.String(),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
impl: Input,
|
||||
|
125
packages/runtime/src/components/chakra-ui/List.tsx
Normal file
125
packages/runtime/src/components/chakra-ui/List.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
Application,
|
||||
createComponent,
|
||||
RuntimeApplication,
|
||||
} from '@meta-ui/core';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { List as BaseList, ListItem as BaseListItem } from '@chakra-ui/react';
|
||||
import { ComponentImplementation } from '../../registry';
|
||||
import { ImplWrapper, resolveAppComponents } from '../../App';
|
||||
import { mapValuesDeep, maskedEval } from '../../store';
|
||||
import { LIST_ITEM_EXP, LIST_ITEM_INDEX_EXP } from '../../constants';
|
||||
import { parseType } from '../../utils/parseType';
|
||||
|
||||
export function parseTypeComponents(
|
||||
c: Application['spec']['components'][0]
|
||||
): RuntimeApplication['spec']['components'][0] {
|
||||
return {
|
||||
...c,
|
||||
children: [],
|
||||
parsedType: parseType(c.type),
|
||||
traits: c.traits.map(t => {
|
||||
return {
|
||||
...t,
|
||||
parsedType: parseType(t.type),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const List: ComponentImplementation<{
|
||||
listData: Static<typeof ListDataPropertySchema>;
|
||||
template: Static<typeof TemplatePropertySchema>;
|
||||
onClick?: () => void;
|
||||
}> = ({ listData, template, app }) => {
|
||||
if (!listData) {
|
||||
return null;
|
||||
}
|
||||
const itemElementMemo = useRef(new Map());
|
||||
const parsedtemplete = template.map(parseTypeComponents);
|
||||
|
||||
const listItems = listData.map((listItem, i) => {
|
||||
// this memo only diff listItem, dosen't compare expressions
|
||||
if (itemElementMemo.current.has(listItem.id)) {
|
||||
if (itemElementMemo.current.get(listItem.id).value === listItem) {
|
||||
return itemElementMemo.current.get(listItem.id).ele;
|
||||
}
|
||||
}
|
||||
|
||||
const evaledTemplate = mapValuesDeep({ parsedtemplete }, ({ value }) => {
|
||||
if (typeof value === 'string') {
|
||||
return maskedEval(value, true, {
|
||||
[LIST_ITEM_EXP]: listItem,
|
||||
[LIST_ITEM_INDEX_EXP]: i,
|
||||
});
|
||||
}
|
||||
return value;
|
||||
}).parsedtemplete;
|
||||
|
||||
const { topLevelComponents, slotComponentsMap } = resolveAppComponents(
|
||||
evaledTemplate,
|
||||
app
|
||||
);
|
||||
|
||||
const componentElements = topLevelComponents.map(c => {
|
||||
return (
|
||||
<ImplWrapper
|
||||
key={c.id}
|
||||
component={c}
|
||||
slotsMap={slotComponentsMap.get(c.id)}
|
||||
targetSlot={null}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const listItemEle = (
|
||||
<BaseListItem key={listItem.id} spacing={3}>
|
||||
{componentElements}
|
||||
</BaseListItem>
|
||||
);
|
||||
|
||||
itemElementMemo.current.set(listItem.id, {
|
||||
value: listItem,
|
||||
ele: listItemEle,
|
||||
});
|
||||
return listItemEle;
|
||||
});
|
||||
|
||||
return <BaseList>{listItems}</BaseList>;
|
||||
};
|
||||
|
||||
const ListDataPropertySchema = Type.Array(
|
||||
Type.Object(Type.String(), Type.String())
|
||||
);
|
||||
const TemplatePropertySchema = Type.Object(
|
||||
Type.String(),
|
||||
Type.Array(Type.Object(Type.String()))
|
||||
);
|
||||
|
||||
export default {
|
||||
...createComponent({
|
||||
version: 'chakra_ui/v1',
|
||||
metadata: {
|
||||
name: 'list',
|
||||
description: 'chakra-ui list',
|
||||
},
|
||||
spec: {
|
||||
properties: [
|
||||
{
|
||||
name: 'listData',
|
||||
...ListDataPropertySchema,
|
||||
},
|
||||
{
|
||||
name: 'template',
|
||||
...TemplatePropertySchema,
|
||||
},
|
||||
],
|
||||
acceptTraits: [],
|
||||
methods: [],
|
||||
state: {},
|
||||
},
|
||||
}),
|
||||
impl: List,
|
||||
};
|
2
packages/runtime/src/constants.ts
Normal file
2
packages/runtime/src/constants.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const LIST_ITEM_EXP = '$listItem';
|
||||
export const LIST_ITEM_INDEX_EXP = '$i';
|
@ -1,5 +1,9 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { RuntimeComponent, RuntimeTrait } from '@meta-ui/core';
|
||||
import {
|
||||
RuntimeApplication,
|
||||
RuntimeComponent,
|
||||
RuntimeTrait,
|
||||
} from '@meta-ui/core';
|
||||
import { SlotsMap } from './App';
|
||||
// components
|
||||
/* --- plain --- */
|
||||
@ -15,6 +19,7 @@ import ChakraUITable from './components/chakra-ui/Table';
|
||||
import ChakraUIInput from './components/chakra-ui/Input';
|
||||
import ChakraUIBox from './components/chakra-ui/Box';
|
||||
import ChakraUIKbd from './components/chakra-ui/Kbd';
|
||||
import ChakraUIKList from './components/chakra-ui/List';
|
||||
import ChakraUINumberInput from './components/chakra-ui/NumberInput';
|
||||
import ChakraUICheckboxGroup from './components/chakra-ui/CheckboxGroup';
|
||||
import ChakraUICheckbox from './components/chakra-ui/Checkbox';
|
||||
@ -26,6 +31,7 @@ import ChakraUIImage from './components/chakra-ui/Image';
|
||||
/* --- lab --- */
|
||||
import LabEditor from './components/lab/Editor';
|
||||
// traits
|
||||
import CoreArrayState from './traits/core/arrayState';
|
||||
import CoreState from './traits/core/state';
|
||||
import CoreEvent from './traits/core/event';
|
||||
import CoreSlot from './traits/core/slot';
|
||||
@ -57,6 +63,7 @@ export type ComponentMergedProps = {
|
||||
style?: CSSProperties;
|
||||
data?: Record<string, any>;
|
||||
callbackMap?: CallbackMap;
|
||||
app?: RuntimeApplication;
|
||||
};
|
||||
|
||||
export type ComponentImplementation<T = any> = React.FC<
|
||||
@ -136,6 +143,7 @@ registry.registerComponent(ChakraUITable);
|
||||
registry.registerComponent(ChakraUIInput);
|
||||
registry.registerComponent(ChakraUIBox);
|
||||
registry.registerComponent(ChakraUIKbd);
|
||||
registry.registerComponent(ChakraUIKList);
|
||||
registry.registerComponent(ChakraUINumberInput);
|
||||
registry.registerComponent(ChakraUICheckbox);
|
||||
registry.registerComponent(ChakraUICheckboxGroup);
|
||||
@ -148,6 +156,7 @@ registry.registerComponent(LabEditor);
|
||||
registry.registerComponent(CoreRouter);
|
||||
|
||||
registry.registerTrait(CoreState);
|
||||
registry.registerTrait(CoreArrayState);
|
||||
registry.registerTrait(CoreEvent);
|
||||
registry.registerTrait(CoreSlot);
|
||||
registry.registerTrait(CoreHidden);
|
||||
|
@ -3,43 +3,87 @@ import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { reactive } from '@vue/reactivity';
|
||||
import { watch } from '@vue-reactivity/watch';
|
||||
import { LIST_ITEM_EXP, LIST_ITEM_INDEX_EXP } from './constants';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
function parseExpression(raw: string): {
|
||||
dynamic: boolean;
|
||||
type ExpChunk = {
|
||||
expression: string;
|
||||
} {
|
||||
if (!raw) {
|
||||
return {
|
||||
dynamic: false,
|
||||
expression: raw,
|
||||
};
|
||||
}
|
||||
const matchArr = raw.match(/{{(.+)}}/);
|
||||
if (!matchArr) {
|
||||
return {
|
||||
dynamic: false,
|
||||
expression: raw,
|
||||
};
|
||||
}
|
||||
return {
|
||||
dynamic: true,
|
||||
expression: matchArr[1],
|
||||
};
|
||||
}
|
||||
|
||||
function isNumeric(x: string | number) {
|
||||
return !isNaN(Number(x));
|
||||
}
|
||||
|
||||
export const stateStore = reactive<Record<string, any>>({});
|
||||
isDynamic: boolean;
|
||||
};
|
||||
|
||||
// TODO: use web worker
|
||||
const builtIn = {
|
||||
dayjs,
|
||||
};
|
||||
function maskedEval(raw: string) {
|
||||
|
||||
function isNumeric(x: string | number) {
|
||||
return !isNaN(Number(x)) && x !== '';
|
||||
}
|
||||
|
||||
export const stateStore = reactive<Record<string, any>>({});
|
||||
export function parseExpression(
|
||||
str: string,
|
||||
parseListItem = false
|
||||
): ExpChunk[] {
|
||||
let l = 0;
|
||||
let r = 0;
|
||||
let isInBrackets = false;
|
||||
const res = [];
|
||||
|
||||
while (r < str.length - 1) {
|
||||
if (!isInBrackets && str.substr(r, 2) === '{{') {
|
||||
if (l !== r) {
|
||||
const substr = str.substring(l, r);
|
||||
res.push({
|
||||
expression: substr,
|
||||
isDynamic: false,
|
||||
});
|
||||
}
|
||||
isInBrackets = true;
|
||||
r += 2;
|
||||
l = r;
|
||||
} else if (isInBrackets && str.substr(r, 2) === '}}') {
|
||||
// remove \n from start and end of substr
|
||||
const substr = str.substring(l, r).replace(/^\s+|\s+$/g, '');
|
||||
const chunk = {
|
||||
expression: substr,
|
||||
isDynamic: true,
|
||||
};
|
||||
// $listItem cannot be evaled in stateStore, so don't mark it as dynamic
|
||||
// unless explicitly pass parseListItem as true
|
||||
if (
|
||||
(substr.includes(LIST_ITEM_EXP) ||
|
||||
substr.includes(LIST_ITEM_INDEX_EXP)) &&
|
||||
!parseListItem
|
||||
) {
|
||||
chunk.expression = `{{${substr}}}`;
|
||||
chunk.isDynamic = false;
|
||||
}
|
||||
res.push(chunk);
|
||||
|
||||
isInBrackets = false;
|
||||
r += 2;
|
||||
l = r;
|
||||
} else {
|
||||
r++;
|
||||
}
|
||||
}
|
||||
|
||||
if (r >= l && l < str.length) {
|
||||
res.push({
|
||||
expression: str.substring(l, r + 1),
|
||||
isDynamic: false,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function maskedEval(
|
||||
raw: string,
|
||||
evalListItem = false,
|
||||
scopeObject = {}
|
||||
) {
|
||||
if (isNumeric(raw)) {
|
||||
return _.toNumber(raw);
|
||||
}
|
||||
@ -50,23 +94,30 @@ function maskedEval(raw: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { dynamic, expression } = parseExpression(raw);
|
||||
const expChunks = parseExpression(raw, evalListItem);
|
||||
const evaled = expChunks.map(({ expression: exp, isDynamic }) => {
|
||||
if (!isDynamic) {
|
||||
return exp;
|
||||
}
|
||||
try {
|
||||
const result = new Function(`with(this) { return ${exp} }`).call({
|
||||
...stateStore,
|
||||
...builtIn,
|
||||
...scopeObject,
|
||||
});
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
Error(`Cannot eval value '${exp}' in '${raw}': ${e.message}`)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
if (!dynamic) {
|
||||
return raw;
|
||||
}
|
||||
try {
|
||||
const result = new Function(`with(this) { return ${expression} }`).call({
|
||||
...stateStore,
|
||||
...builtIn,
|
||||
});
|
||||
return result;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return evaled.length === 1 ? evaled[0] : evaled.join('');
|
||||
}
|
||||
|
||||
const mapValuesDeep = (
|
||||
export const mapValuesDeep = (
|
||||
obj: any,
|
||||
fn: (params: {
|
||||
value: any;
|
||||
@ -97,10 +148,11 @@ export function deepEval(
|
||||
|
||||
const evaluated = mapValuesDeep(obj, ({ value: v, path }) => {
|
||||
if (typeof v === 'string') {
|
||||
const { dynamic } = parseExpression(v);
|
||||
const isDynamicExpression = parseExpression(v).some(
|
||||
({ isDynamic }) => isDynamic
|
||||
);
|
||||
const result = maskedEval(v);
|
||||
|
||||
if (dynamic && watcher) {
|
||||
if (isDynamicExpression && watcher) {
|
||||
const stop = watch(
|
||||
() => {
|
||||
return maskedEval(v);
|
||||
|
105
packages/runtime/src/traits/core/arrayState.tsx
Normal file
105
packages/runtime/src/traits/core/arrayState.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { createTrait } from '@meta-ui/core';
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { TraitImplementation } from '../../registry';
|
||||
import { stateStore } from '../../store';
|
||||
|
||||
const HasInitializedMap = new Map<string, boolean>();
|
||||
|
||||
type KeyValue = { key: string; value: unknown };
|
||||
|
||||
const ArrayStateTrait: TraitImplementation<{
|
||||
key: Static<typeof KeyPropertySchema>;
|
||||
initialValue: Static<typeof InitialValuePropertySchema>;
|
||||
}> = ({ key, initialValue, componentId, mergeState, subscribeMethods }) => {
|
||||
const hashId = `#${componentId}@${key}`;
|
||||
let hasInitialized = HasInitializedMap.get(hashId);
|
||||
|
||||
if (!hasInitialized) {
|
||||
mergeState({ [key]: initialValue });
|
||||
|
||||
const methods = {
|
||||
setArray({ key, value }: KeyValue) {
|
||||
mergeState({ [key]: value });
|
||||
},
|
||||
deleteItemByIndex({ key, index }: { key: string; index: number }) {
|
||||
const _arr = [...stateStore[componentId][key]];
|
||||
_arr.splice(index, 1);
|
||||
mergeState({ [key]: _arr });
|
||||
},
|
||||
deleteItemById({
|
||||
key,
|
||||
itemIdKey,
|
||||
itemId,
|
||||
}: {
|
||||
key: string;
|
||||
itemIdKey: string;
|
||||
itemId: string;
|
||||
}) {
|
||||
const _arr = [...stateStore[componentId][key]].filter(item => {
|
||||
return item[itemIdKey] !== itemId;
|
||||
});
|
||||
mergeState({ [key]: _arr });
|
||||
},
|
||||
};
|
||||
subscribeMethods(methods);
|
||||
HasInitializedMap.set(hashId, true);
|
||||
}
|
||||
|
||||
return {
|
||||
props: null,
|
||||
};
|
||||
};
|
||||
|
||||
const KeyPropertySchema = Type.String();
|
||||
const InitialValuePropertySchema = Type.Array(Type.Any());
|
||||
|
||||
export default {
|
||||
...createTrait({
|
||||
version: 'core/v1',
|
||||
metadata: {
|
||||
name: 'arrayState',
|
||||
description: 'add array state to component',
|
||||
},
|
||||
spec: {
|
||||
properties: [
|
||||
{
|
||||
name: 'key',
|
||||
...KeyPropertySchema,
|
||||
},
|
||||
{
|
||||
name: 'initialValue',
|
||||
...InitialValuePropertySchema,
|
||||
},
|
||||
],
|
||||
state: Type.Any(),
|
||||
methods: [
|
||||
{
|
||||
name: 'setArray',
|
||||
parameters: Type.Object({
|
||||
key: Type.String(),
|
||||
value: Type.Array(Type.Any()),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'deleteItemByIndex',
|
||||
parameters: Type.Object({
|
||||
key: Type.String(),
|
||||
index: Type.Integer(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'deleteItemById',
|
||||
parameters: Type.Object({
|
||||
key: Type.String(),
|
||||
itemIdKey: Type.String(),
|
||||
itemId: Type.String(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'reset',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
impl: ArrayStateTrait,
|
||||
};
|
16
packages/runtime/src/utils/parseType.ts
Normal file
16
packages/runtime/src/utils/parseType.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// 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,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user