feat: Form 支持组合校验 (#1476)

* feat: Form 支持组合校验

* fix doc

* fix doc
This commit is contained in:
RickCole 2021-01-29 12:56:50 +08:00 committed by GitHub
parent 2af0c423e4
commit bc788a9045
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 145 additions and 67 deletions

View File

@ -495,7 +495,7 @@ Form 支持轮询初始化接口,步骤如下:
```schema: scope="body"
{
"type": "form",
"initApi": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/saveForm",
"api": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/saveForm",
"title": "用户信息",
"controls": [
{
@ -607,6 +607,39 @@ Form 支持轮询初始化接口,步骤如下:
如果决定结束轮询的标识字段名不是 `finished`,请设置`finishedField`属性,比如:`"finishedField": "is_success"`
## 表单校验
一般可以通过在[表单项格式校验](./formitem#%E6%A0%BC%E5%BC%8F%E6%A0%A1%E9%AA%8C)中,配置校验规则完成校验,但是有时候,我们需要组合多个表单项实现一些校验,这时可以通过配置 `rules` 来实现组合校验。
例如下例,我们想校验 `a``b` 表单项不可以同时有值,否则就报错,则可以进行如下配置:
```schema:scope="body"
{
"type": "form",
"api": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/saveForm",
"rules": [
{
"rule": "!(data.a && data.b)",
"message": "a 和 b 不能同时有值"
}
],
"controls": [
{
"type": "text",
"name": "a",
"label": "A"
},
{
"type": "text",
"name": "b",
"label": "B"
}
]
}
```
> `rule` 编写使用 [表达式](../../concepts/expression)
## 重置表单
配置`"type": "reset"`或者`"actionType": "reset"`的按钮,可以实现点击重置表单项值。
@ -828,7 +861,7 @@ Form 支持轮询初始化接口,步骤如下:
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------------------------- | ---------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| --------------------------- | ----------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| type | `string` | | `"form"` 指定为 Form 渲染器 |
| name | `string` | | 设置一个名字后,方便其他组件与其通信 |
| mode | `string` | `normal` | 表单展示方式,可以是:`normal`、`horizontal` 或者 `inline` |
@ -847,6 +880,7 @@ Form 支持轮询初始化接口,步骤如下:
| panelClassName | `string` | | 外层 panel 的类名 |
| api | [API](../../types/api) | | Form 用来保存数据的 api。 |
| initApi | [API](../../types/api) | | Form 用来获取初始数据的 api。 |
| rules | Array<{rule:string;message:string}> | | 表单组合校验规则 |
| interval | `number` | `3000` | 刷新时间(最低 3000) |
| silentPolling | `boolean` | `false` | 配置刷新时是否显示加载动画 |
| stopAutoRefreshWhen | `string` | `""` | 通过[表达式](./Types.md#表达式) 来配置停止刷新的条件 |

View File

@ -274,6 +274,14 @@ export interface FormSchema extends BaseSchema {
*
*/
promptPageLeaveMessage?: string;
/**
*
*/
rules: Array<{
rule: string;
message: string;
}>;
}
export type FormGroup = FormSchema & {
@ -307,6 +315,10 @@ export interface FormProps extends RendererProps, Omit<FormSchema, 'mode'> {
saveFailed?: string;
validateFailed?: string;
};
rules: Array<{
rule: string;
message: string;
}>;
lazyChange?: boolean; // 表单项的
formLazyChange?: boolean; // 表单的
}
@ -373,6 +385,7 @@ export default class Form extends React.Component<FormProps, object> {
} = {};
asyncCancel: () => void;
disposeOnValidate: () => void;
disposeRulesValidate: () => void;
shouldLoadInitApi: boolean = false;
timer: NodeJS.Timeout;
mounted: boolean;
@ -441,7 +454,8 @@ export default class Form extends React.Component<FormProps, object> {
messages: {fetchSuccess, fetchFailed},
onValidate,
promptPageLeave,
env
env,
rules
} = this.props;
this.mounted = true;
@ -461,23 +475,38 @@ export default class Form extends React.Component<FormProps, object> {
return;
}
// 在setError之前提前把残留的error信息清除掉否则每次onValidate后都会一直把报错 append 上去
items.forEach(item => item.clearError());
if (msg) {
msg = Array.isArray(msg) ? msg : [msg];
items.forEach(item => item.addError(msg));
} else {
items.forEach(item => item.clearError());
}
delete result[key];
});
isEmpty(result)
? store.clearRestErrors()
: store.setRestErrors(result);
? store.clearRestError()
: store.setRestError(Object.keys(result).map(key => result[key]));
}
});
}
if (Array.isArray(rules) && rules.length) {
this.disposeRulesValidate = this.addHook(() => {
if (!store.valid) {
return;
}
rules.forEach(
item =>
!evalExpression(item.rule, store.data) &&
store.addRestError(item.message)
);
});
}
if (isEffectiveApi(initApi, store.data, initFetch, initFetchOn)) {
store
.fetchInitData(initApi as any, store.data, {
@ -543,6 +572,7 @@ export default class Form extends React.Component<FormProps, object> {
this.lazyHandleChange.cancel();
this.asyncCancel && this.asyncCancel();
this.disposeOnValidate && this.disposeOnValidate();
this.disposeRulesValidate && this.disposeRulesValidate();
this.componentCache.dispose();
window.removeEventListener('beforeunload', this.beforePageUnload);
this.unBlockRouting?.();
@ -767,7 +797,7 @@ export default class Form extends React.Component<FormProps, object> {
onChange &&
onChange(store.data, difference(store.data, store.pristine), this.props);
store.clearRestErrors();
store.clearRestError();
(submit || submitOnChange) &&
this.handleAction(
@ -1341,6 +1371,8 @@ export default class Form extends React.Component<FormProps, object> {
render
} = this.props;
const {restError} = store;
const WrapperComponent =
this.props.wrapperComponent ||
(/(?:\/|^)form\//.test($path as string) ? 'div' : 'form');
@ -1365,11 +1397,11 @@ export default class Form extends React.Component<FormProps, object> {
controls
})}
{/* 显示接口返回的 errors 中没有映射上的 */}
{store.restErrors ? (
<ul className={cx('Form-restErrors', 'Form-feedback')}>
{Object.keys(store.restErrors).map(key => (
<li key={key}>{store.restErrors[key]}</li>
{/* 显示没有映射上的 errors */}
{restError && restError.length ? (
<ul className={cx('Form-restError', 'Form-feedback')}>
{restError.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
) : null}

View File

@ -41,7 +41,7 @@ export const FormStore = ServiceStore.named('FormStore')
itemsRef: types.optional(types.array(types.string), []),
canAccessSuperData: true,
persistData: false,
restErrors: types.frozen() // 没有映射到表达项上的 errors
restError: types.optional(types.array(types.string), []) // 没有映射到表达项上的 errors
})
.views(self => {
function getItems() {
@ -94,7 +94,10 @@ export const FormStore = ServiceStore.named('FormStore')
},
get valid() {
return getItems().every(item => item.valid);
return (
getItems().every(item => item.valid) &&
(!self.restError || !self.restError.length)
);
},
get isPristine() {
@ -226,12 +229,19 @@ export const FormStore = ServiceStore.named('FormStore')
}
);
function setRestErrors(errors: any) {
self.restErrors = errors;
function setRestError(errors: string[]) {
self.restError.replace(errors);
}
function clearRestErrors() {
setRestErrors(null);
function addRestError(msg: string | Array<string>) {
const msgs: Array<string> = Array.isArray(msg) ? msg : [msg];
msgs.forEach(msg => {
self.restError.push(msg);
});
}
function clearRestError() {
setRestError([]);
}
const saveRemote: (
@ -243,7 +253,7 @@ export const FormStore = ServiceStore.named('FormStore')
data: object,
options: fetchOptions = {}
) {
clearRestErrors();
clearRestError();
try {
options = {
@ -308,7 +318,8 @@ export const FormStore = ServiceStore.named('FormStore')
});
// 没有映射上的error信息加在msg后显示出来
!isEmpty(errors) && setRestErrors(errors);
!isEmpty(errors) &&
setRestError(Object.keys(errors).map(key => errors[key]));
self.updateMessage(
json.msg ??
@ -413,7 +424,7 @@ export const FormStore = ServiceStore.named('FormStore')
try {
let valid = yield validate(hooks);
if (!valid || self.restErrors) {
if (!valid) {
const msg = failedMessage ?? self.__('Form.validateFailed');
msg && getEnv(self).notify('error', msg);
throw new Error(self.__('Form.validateFailed'));
@ -581,8 +592,9 @@ export const FormStore = ServiceStore.named('FormStore')
onChildStoreDispose,
updateSavedData,
getItemsByPath,
setRestErrors,
clearRestErrors,
setRestError,
addRestError,
clearRestError,
beforeDestroy() {
syncOptions.cancel();
setPersistData.cancel();