add dialog form

This commit is contained in:
Bowen Tan 2021-09-18 14:38:04 +08:00
parent 288a432923
commit 8eaec48b97
5 changed files with 630 additions and 36 deletions

View File

@ -0,0 +1,563 @@
<!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: input validation component</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import renderApp from '../../src/main.tsx';
renderApp({
version: 'example/v1',
metadata: {
name: 'dialog form',
description: 'dialog form example',
},
spec: {
components: [
{
id: 'fetchVolumes',
type: 'core/v1/dummy',
properties: {},
traits: [
{
type: 'core/v1/fetch',
properties: {
name: 'query',
url: 'https://61373521eac1410017c18209.mockapi.io/Volume',
method: 'get',
lazy: false,
},
},
],
},
{
id: 'createVolume',
type: 'core/v1/dummy',
properties: {},
traits: [
{
type: 'core/v1/fetch',
properties: {
url: 'https://61373521eac1410017c18209.mockapi.io/Volume',
method: 'post',
lazy: true,
headers: [
{ key: 'Content-Type', value: 'application/json' },
],
body: '{{ form.data }}',
onComplete: [
{
componentId: '$utils',
method: {
name: 'toast.open',
parameters: {
id: 'createSuccessToast',
title: '太棒了',
description: '创建虚拟卷成功了',
position: 'bottom-right',
duration: null,
isClosable: true,
},
},
},
{
componentId: 'editDialog',
method: {
name: 'cancelDialog',
},
},
{
componentId: 'form',
method: {
name: 'resetForm',
},
},
{
componentId: 'fetchVolumes',
method: {
name: 'triggerFetch',
parameters: 'query',
},
wait: {},
disabled: 'false',
},
],
},
},
],
},
{
id: 'deleteVolume',
type: 'core/v1/dummy',
properties: {},
traits: [
{
type: 'core/v1/fetch',
properties: {
url: 'https://61373521eac1410017c18209.mockapi.io/Volume/{{ table.selectedItem ? table.selectedItem.id : "" }}',
method: 'delete',
lazy: true,
onComplete: [
{
componentId: 'fetchVolumes',
method: {
name: 'triggerFetch',
parameters: 'query',
},
wait: {},
disabled: 'false',
},
],
},
},
],
},
{
id: 'root',
type: 'chakra_ui/v1/root',
properties: {},
traits: [],
},
{
id: 'editDialog',
type: 'chakra_ui/v1/dialog',
properties: {
title: 'This is a dialog',
confirmButton: {
text: '保存',
colorScheme: 'purple',
},
cancelButton: {
text: '取消',
},
disableConfirm: '{{ form.disableSubmit }}',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'root',
slot: 'root',
},
},
},
// dialog events
{
type: 'core/v1/event',
parsedType: {
version: 'core/v1',
name: 'event',
},
properties: {
events: [
// when click confirm
{
event: 'confirmDialog',
componentId: 'createVolume',
method: {
name: 'triggerFetch',
},
wait: {},
disabled: false,
},
],
},
},
],
},
{
id: 'table',
type: 'chakra_ui/v1/table',
properties: {
data: '{{ fetchVolumes.fetch.data }}',
majorKey: 'id',
rowsPerPage: 5,
columns: [
{
key: 'id',
title: 'ID',
type: 'text',
},
{
key: 'name',
title: '名称',
type: 'text',
},
{
key: 'type',
title: '类别',
type: 'text',
displayValue:
'{{$listItem.type === "sharing" ? "共享虚拟卷" : "虚拟卷"}}',
},
{
key: 'size',
title: '容量',
type: 'text',
displayValue: '{{$listItem.size}} GiB',
},
{
key: 'policy',
title: '存储策略',
type: 'text',
},
{
key: 'isActive',
title: '是否激活',
type: 'text',
displayValue: '{{$listItem.isActive ? "是" : "否"}}',
},
{
key: 'operation',
title: '操作',
type: 'button',
buttonConfig: {
text: '删除',
events: [
{
componentId: 'deleteVolume',
method: {
name: 'triggerFetch',
},
},
],
},
},
{
key: 'edit',
title: '创建',
type: 'button',
buttonConfig: {
text: '创建',
events: [
{
componentId: 'editDialog',
method: {
name: 'openDialog',
parameters: {
title: '创建虚拟卷',
},
},
},
],
},
},
],
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'root',
slot: 'root',
},
},
},
],
},
{
id: 'form',
type: 'chakra_ui/v1/form',
properties: {
hideSubmit: true,
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'editDialog',
slot: 'content',
},
},
},
],
},
{
id: 'nameFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '名称',
fieldName: 'name',
isRequired: true,
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'nameInput',
type: 'chakra_ui/v1/input',
properties: {
defaultValue:
'{{ table.selectedItem ? table.selectedItem.name : "" }}',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'nameFormControl',
slot: 'content',
},
},
},
{
type: 'core/v1/validation',
properties: {
value: '{{ nameInput.value || "" }}',
maxLength: 10,
minLength: 2,
},
},
],
},
{
id: 'typeFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '类型',
fieldName: 'type',
helperText:
'共享虚拟卷支持被多台虚拟机同时挂载。类型创建后不可修改。',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'typeRadioGroup',
type: 'chakra_ui/v1/radio_group',
properties: {
defaultValue:
'{{ table.selectedItem ? table.selectedItem.type : "notSharing" }}',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'typeFormControl',
slot: 'content',
},
},
},
],
},
{
id: 'radio1',
type: 'chakra_ui/v1/radio',
properties: {
text: {
raw: '虚拟卷',
format: 'plain',
},
value: 'notSharing',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'typeRadioGroup',
slot: 'content',
},
},
},
],
},
{
id: 'radio2',
type: 'chakra_ui/v1/radio',
properties: {
text: {
raw: '共享虚拟卷',
format: 'plain',
},
value: 'sharing',
size: 'md',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'typeRadioGroup',
slot: 'content',
},
},
},
],
},
{
id: 'sizeFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '容量',
fieldName: 'size',
isRequired: true,
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'sizeInput',
type: 'chakra_ui/v1/number_input',
properties: {
defaultValue:
'{{ table.selectedItem ? table.selectedItem.size : 0 }}',
min: 0,
max: 100,
step: 5,
precision: 2,
clampValueOnBlur: false,
allowMouseWheel: true,
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'sizeFormControl',
slot: 'content',
},
},
},
],
},
{
id: 'policyFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '存储策略',
fieldName: 'policy',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'policySelect',
type: 'chakra_ui/v1/select',
properties: {
defaultValue:
'{{ table.selectedItem ? table.selectedItem.policy : "2thin" }}',
options: [
{
value: '2thin',
label: '2 副本,精简置备',
},
{
value: '3thin',
label: '3 副本,精简置备',
},
{
value: '2thick',
label: '2 副本,厚置备',
},
{
value: '3thick',
label: '3 副本,厚置备',
},
],
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'policyFormControl',
slot: 'content',
},
},
},
],
},
{
id: 'isActiveFormControl',
type: 'chakra_ui/v1/formControl',
properties: {
label: '激活',
fieldName: 'isActive',
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'form',
slot: 'content',
},
},
},
],
},
{
id: 'checkbox',
type: 'chakra_ui/v1/checkbox',
properties: {
value: 'isActive',
defaultIsChecked:
'{{table.selectedItem ? !!table.selectedItem.isActive : false}}',
text: {
raw: '激活',
format: 'plain',
},
},
traits: [
{
type: 'core/v1/slot',
properties: {
container: {
id: 'isActiveFormControl',
slot: 'content',
},
},
},
],
},
],
},
});
</script>
</body>
</html>

View File

@ -13,8 +13,8 @@
renderApp({
version: 'example/v1',
metadata: {
name: 'api form',
description: 'api form example',
name: 'table form',
description: 'table form example',
},
spec: {
components: [

View File

@ -15,6 +15,7 @@ import Slot from '../_internal/Slot';
import { ColorSchemePropertySchema } from './Types/ColorScheme';
const TitlePropertySchema = Type.Optional(Type.String());
const DisableConfirmPropertySchema = Type.Optional(Type.Boolean());
const HandleButtonPropertySchema = Type.Object({
text: Type.Optional(Type.String()),
@ -25,11 +26,13 @@ const Dialog: ComponentImplementation<{
title?: Static<typeof TitlePropertySchema>;
confirmButton?: Static<typeof HandleButtonPropertySchema>;
cancelButton?: Static<typeof HandleButtonPropertySchema>;
disableConfirm?: Static<typeof DisableConfirmPropertySchema>;
}> = ({
slotsMap,
subscribeMethods,
callbackMap: callbacks,
title: customerTitle,
disableConfirm,
confirmButton = {
text: 'confirm',
colorScheme: 'red',
@ -80,6 +83,7 @@ const Dialog: ComponentImplementation<{
{cancelButton.text}
</Button>
<Button
disabled={disableConfirm}
colorScheme={confirmButton.colorScheme}
onClick={callbacks?.confirmDialog}
ml={3}>
@ -114,6 +118,10 @@ export default {
name: 'cancelButton',
...HandleButtonPropertySchema,
},
{
name: 'disableConfirm',
...DisableConfirmPropertySchema,
},
],
acceptTraits: [],
state: {},

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Static, Type } from '@sinclair/typebox';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Type } from '@sinclair/typebox';
import { createComponent } from '@meta-ui/core';
import { ComponentImplementation } from '../../registry';
import Slot from '../_internal/Slot';
@ -7,14 +7,10 @@ import { Button } from '@chakra-ui/react';
import { stateStore } from '../../store';
import { watch } from '@vue-reactivity/watch';
import { apiService } from '../../api-service';
import { CheckboxStateSchema } from './Checkbox';
const FormImpl: ComponentImplementation<Record<string, string>> = ({
mergeState,
subscribeMethods,
slotsMap,
callbackMap,
}) => {
const FormImpl: ComponentImplementation<{
hideSubmit?: boolean;
}> = ({ mergeState, subscribeMethods, hideSubmit, slotsMap, callbackMap }) => {
// 理论上说slotsMap是永远不变的
const formControlIds = useMemo<string[]>(() => {
return (
@ -25,6 +21,8 @@ const FormImpl: ComponentImplementation<Record<string, string>> = ({
}, [slotsMap]);
const [invalidArray, setInvalidArray] = useState<boolean[]>([]);
const [disableSubmit, setDisableSubmit] = useState<boolean>(false);
const dataRef = useRef<Record<string, any>>({});
useEffect(() => {
setInvalidArray(
@ -34,6 +32,14 @@ const FormImpl: ComponentImplementation<Record<string, string>> = ({
);
}, []);
useEffect(() => {
const disable = invalidArray.some(v => v);
setDisableSubmit(disable);
mergeState({
disableSubmit: disable,
});
}, [invalidArray]);
useEffect(() => {
subscribeMethods({
resetForm() {
@ -51,7 +57,8 @@ const FormImpl: ComponentImplementation<Record<string, string>> = ({
useEffect(() => {
const stops: ReturnType<typeof watch>[] = [];
formControlIds.forEach((fcId, i) => {
const stop = watch(
// watch isInvalid
let stop = watch(
() => {
return stateStore[fcId].isInvalid;
},
@ -64,6 +71,19 @@ const FormImpl: ComponentImplementation<Record<string, string>> = ({
}
);
stops.push(stop);
// watch value
stop = watch(
() => {
return stateStore[fcId].value;
},
newV => {
const fcState = stateStore[fcId];
dataRef.current[fcState.fieldName] = newV;
mergeState({ data: { ...dataRef.current } });
}
);
stops.push(stop);
});
return () => {
@ -71,34 +91,20 @@ const FormImpl: ComponentImplementation<Record<string, string>> = ({
s();
});
};
}, []);
}, [formControlIds]);
const onSubmit = () => {
const data: Record<string, string | boolean> = {};
formControlIds.forEach(fcId => {
const fcState = stateStore[fcId];
const fieldName = fcState.fieldName;
if (stateStore[fcState.inputId].checked !== undefined) {
// special treatment for checkbox
data[fieldName] = (
stateStore[fcState.inputId] as Static<typeof CheckboxStateSchema>
).checked;
} else {
data[fieldName] = stateStore[fcState.inputId].value;
}
});
mergeState({
data,
});
callbackMap?.onSubmit();
};
return (
<form>
<Slot slotsMap={slotsMap} slot="content" />
<Button disabled={invalidArray.some(v => v)} onClick={onSubmit}>
</Button>
{hideSubmit ? undefined : (
<Button disabled={disableSubmit} onClick={onSubmit}>
</Button>
)}
</form>
);
};
@ -111,10 +117,16 @@ export default {
description: 'chakra-ui form',
},
spec: {
properties: [],
properties: [
{
name: 'hideSubmit',
...Type.Boolean(),
},
],
acceptTraits: [],
state: Type.Object({
data: Type.Any(),
disableSubmit: Type.Boolean(),
}),
methods: [
{

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import _ from 'lodash';
import { createComponent } from '@meta-ui/core';
import { Type } from '@sinclair/typebox';
import { Static, Type } from '@sinclair/typebox';
import {
FormControl,
FormErrorMessage,
@ -12,6 +12,7 @@ import { ComponentImplementation } from '../../registry';
import Slot from '../_internal/Slot';
import { watch } from '@vue-reactivity/watch';
import { stateStore } from '../../store';
import { CheckboxStateSchema } from './Checkbox';
const FormControlImpl: ComponentImplementation<{
label: string;
@ -30,14 +31,22 @@ const FormControlImpl: ComponentImplementation<{
useEffect(() => {
const inputId = _.first(slotsMap?.get('content'))?.id || '';
return watch(
const stop = watch(
() => {
return stateStore[inputId].value;
if (stateStore[inputId].checked !== undefined) {
// special treatment for checkbox
return (stateStore[inputId] as Static<typeof CheckboxStateSchema>)
.checked;
} else {
return stateStore[inputId].value;
}
},
newV => {
setInputValue(newV);
}
);
setInputValue(stateStore[inputId].value);
return stop;
}, [slotsMap, setInputValue]);
useEffect(() => {
@ -61,6 +70,7 @@ const FormControlImpl: ComponentImplementation<{
inputId: _.first(slotsMap?.get('content'))?.id || '',
fieldName,
isInvalid: !!(isInvalid || (!inputValue && isRequired)),
value: inputValue,
});
}, [slotsMap, fieldName, isInvalid, isRequired, inputValue]);
@ -107,6 +117,7 @@ export default {
inputId: Type.String(),
fieldName: Type.String(),
isInvalid: Type.Boolean(),
value: Type.Any(),
}),
methods: [],
},