Merge pull request #53 from webzard-io/feat/list

implement List component
This commit is contained in:
yz-yu 2021-09-12 18:15:15 +08:00 committed by GitHub
commit 646fdf2c89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2461 additions and 123 deletions

View File

@ -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/"],

View File

@ -0,0 +1,7 @@
// only for jest
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View File

@ -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": {

View 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 },
]);
});
});

View File

@ -0,0 +1,7 @@
// only for jest
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View 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>

View File

@ -0,0 +1,3 @@
module.exports = {
...require("../../config/jest.config"),
};

View File

@ -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"
}

View File

@ -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]
);

View File

@ -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,

View 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,
};

View File

@ -0,0 +1,2 @@
export const LIST_ITEM_EXP = '$listItem';
export const LIST_ITEM_INDEX_EXP = '$i';

View File

@ -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);

View File

@ -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);

View 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,
};

View 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,
};
}

1599
yarn.lock

File diff suppressed because it is too large Load Diff