mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +08:00
feat: InputGroup报错展示优化 (#5803)
This commit is contained in:
parent
329518f88a
commit
6f83be8a5a
@ -96,12 +96,56 @@ order: 28
|
||||
|
||||
input-group 配置校验方法较为特殊,需要配置下面步骤:
|
||||
|
||||
1. input-group 上配置任意`name`值
|
||||
1. input-group 上配置任意`name`值 (必填, 否则表单内存在多个输入组合时无法定位)
|
||||
2. input-group 的 body 内配置的表单项上配置校验规则
|
||||
3. 如果 input-group 的子元素配置了`label`, 则会在校验失败时作为标识符展示, 否则仅使用索引值作为标识符
|
||||
4. 单个子元素多条校验信息会使用`; `分隔
|
||||
5. 可以使用`"errorMode": "full" | "partial"`设置错误提示风格, `full`整体飘红, `partial`仅错误元素飘红, 默认为`"full"`
|
||||
|
||||
> 细粒度错误提示需`2.7.1`及以上版本
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"api": "/api/mock2/form/saveForm",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-group",
|
||||
"name": "input-group",
|
||||
"label": "输入组合校验",
|
||||
"body": [
|
||||
{
|
||||
"type": "input-text",
|
||||
"placeholder": "请输入长度不超过6的数字类型",
|
||||
"name": "group-input1",
|
||||
"label": "子元素一",
|
||||
"validations": {
|
||||
"isNumeric": true,
|
||||
"maxLength": 6
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "input-text",
|
||||
"placeholder": "请输入长度不少于5的文本",
|
||||
"name": "group-input2",
|
||||
"required": true,
|
||||
"validations": {
|
||||
"minLength": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 属性表
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
| --------- | --------------------------- | ------ | ---------- |
|
||||
| className | `string` | | CSS 类名 |
|
||||
| body | Array<[表单项](./formitem)> | | 表单项集合 |
|
||||
| 属性名 | 类型 | 默认值 | 说明 | 版本 |
|
||||
| ---------------- | ----------------------------------------- | -------- | ----------------------------------------------------- | ------- |
|
||||
| className | `string` | | CSS 类名 | |
|
||||
| body | Array<[表单项](./formitem)> | | 表单项集合 | |
|
||||
| validationConfig | `Record<'errorMode' \| 'delimiter', any>` | - | 校验相关配置, 具体配置属性如下 | `2.7.3` |
|
||||
| +errorMode | `"full" \| "partial"` | `"full"` | 错误提示风格, `full`整体飘红, `partial`仅错误元素飘红 | `2.7.3` |
|
||||
| +delimiter | `string` | `"; "` | 单个子元素多条校验信息的分隔符 | `2.7.3` |
|
||||
|
@ -876,6 +876,7 @@ export class FormItemWrap extends React.Component<FormItemProps> {
|
||||
} = this.props;
|
||||
|
||||
const mobileUI = useMobileUI && isMobile();
|
||||
|
||||
if (renderControl) {
|
||||
const controlSize = size || defaultSize;
|
||||
return renderControl({
|
||||
|
@ -118,6 +118,7 @@ export function wrapControl<
|
||||
store,
|
||||
onChange,
|
||||
data,
|
||||
inputGroupControl,
|
||||
$schema: {
|
||||
name,
|
||||
id,
|
||||
@ -201,7 +202,8 @@ export function wrapControl<
|
||||
minLength,
|
||||
maxLength,
|
||||
validateOnChange,
|
||||
label
|
||||
label,
|
||||
inputGroupControl
|
||||
});
|
||||
|
||||
// issue 这个逻辑应该在 combo 里面自己实现。
|
||||
@ -335,7 +337,8 @@ export function wrapControl<
|
||||
validateApi: props.$schema.validateApi,
|
||||
minLength: props.$schema.minLength,
|
||||
maxLength: props.$schema.maxLength,
|
||||
label: props.$schema.label
|
||||
label: props.$schema.label,
|
||||
inputGroupControl: props?.inputGroupControl
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -89,6 +89,28 @@ export const FormStore = ServiceStore.named('FormStore')
|
||||
return formItems;
|
||||
},
|
||||
|
||||
/** 获取InputGroup的子元素 */
|
||||
get inputGroupItems() {
|
||||
const formItems: Record<string, IFormItemStore[]> = {};
|
||||
const children = self.children.concat();
|
||||
|
||||
while (children.length) {
|
||||
const current = children.shift();
|
||||
|
||||
if (current.inputGroupControl && current.inputGroupControl?.name) {
|
||||
const controlName = current.inputGroupControl?.name as string;
|
||||
|
||||
if (formItems.hasOwnProperty(controlName)) {
|
||||
formItems[controlName].push(current);
|
||||
} else {
|
||||
formItems[controlName] = [current];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formItems;
|
||||
},
|
||||
|
||||
get errors() {
|
||||
let errors: {
|
||||
[propName: string]: Array<string>;
|
||||
|
@ -15,6 +15,7 @@ import {evalExpression} from '../utils/tpl';
|
||||
import {buildApi, isEffectiveApi} from '../utils/api';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import {
|
||||
isObject,
|
||||
isArrayChildrenModified,
|
||||
createObject,
|
||||
isObjectShallowModified,
|
||||
@ -87,7 +88,9 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
dialogOpen: false,
|
||||
dialogData: types.frozen(),
|
||||
resetValue: types.optional(types.frozen(), ''),
|
||||
validateOnChange: false
|
||||
validateOnChange: false,
|
||||
/** 当前表单项所属的InputGroup父元素, 用于收集InputGroup的子元素 */
|
||||
inputGroupControl: types.optional(types.frozen(), {})
|
||||
})
|
||||
.views(self => {
|
||||
function getForm(): any {
|
||||
@ -236,7 +239,8 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
maxLength,
|
||||
minLength,
|
||||
validateOnChange,
|
||||
label
|
||||
label,
|
||||
inputGroupControl
|
||||
}: {
|
||||
required?: boolean;
|
||||
unique?: boolean;
|
||||
@ -260,6 +264,11 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
maxLength?: number;
|
||||
validateOnChange?: boolean;
|
||||
label?: string;
|
||||
inputGroupControl?: {
|
||||
name: string;
|
||||
path: string;
|
||||
[propsName: string]: any;
|
||||
};
|
||||
}) {
|
||||
if (typeof rules === 'string') {
|
||||
rules = str2rules(rules);
|
||||
@ -289,6 +298,9 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
(self.validateOnChange = !!validateOnChange);
|
||||
typeof label === 'string' && (self.label = label);
|
||||
self.isValueSchemaExp = !!isValueSchemaExp;
|
||||
isObject(inputGroupControl) &&
|
||||
inputGroupControl?.name != null &&
|
||||
(self.inputGroupControl = inputGroupControl);
|
||||
|
||||
rules = {
|
||||
...rules,
|
||||
|
@ -156,3 +156,14 @@
|
||||
.#{$ns}InputGroup:not(.is-inline) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.#{$ns}InputGroup-validation--full.is-error > .#{$ns}Form-control {
|
||||
border-color: var(--Form-input-onError-borderColor);
|
||||
transition: all var(--animation-duration);
|
||||
}
|
||||
|
||||
.#{$ns}InputGroup-validation--partial.is-error > .#{$ns}Form-control.is-error {
|
||||
border-color: var(--Form-input-onError-borderColor);
|
||||
background: var(--Form-input-onError-bg);
|
||||
transition: all var(--animation-duration);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Renderer:fieldSet 1`] = `
|
||||
exports[`Renderer:InputGroup 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="cxd-Panel cxd-Panel--default cxd-Panel--form"
|
||||
@ -53,7 +53,7 @@ exports[`Renderer:fieldSet 1`] = `
|
||||
class="cxd-Form-value"
|
||||
>
|
||||
<div
|
||||
class="cxd-InputGroup cxd-Form-control"
|
||||
class="cxd-InputGroup cxd-InputGroup-validation--full cxd-Form-control"
|
||||
>
|
||||
<div
|
||||
class="cxd-TextControl-input cxd-Form-control"
|
||||
|
@ -1,10 +1,28 @@
|
||||
import React = require('react');
|
||||
import {render, fireEvent} from '@testing-library/react';
|
||||
import '../../../src';
|
||||
import {render as amisRender} from '../../../src';
|
||||
import {makeEnv} from '../../helper';
|
||||
/**
|
||||
* 组件名称: InputGroup 输入组合
|
||||
* 单测内容:
|
||||
* 1. 基础使用
|
||||
* 2. 校验配置
|
||||
*
|
||||
*/
|
||||
|
||||
test('Renderer:fieldSet', async () => {
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
screen,
|
||||
waitFor
|
||||
} from '@testing-library/react';
|
||||
import '../../../src';
|
||||
import {render as amisRender, clearStoresCache} from '../../../src';
|
||||
import {makeEnv, wait} from '../../helper';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
clearStoresCache();
|
||||
});
|
||||
|
||||
test('Renderer:InputGroup', async () => {
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
@ -50,3 +68,110 @@ test('Renderer:fieldSet', async () => {
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const setup = (schema = {}, props = {}, env = {}) => {
|
||||
const onSubmit = jest.fn();
|
||||
const submitBtnText = 'Submit';
|
||||
return render(
|
||||
amisRender(
|
||||
{
|
||||
type: 'form',
|
||||
api: '/api/mock2/form/saveForm',
|
||||
submitText: submitBtnText,
|
||||
body: [
|
||||
{
|
||||
type: 'input-group',
|
||||
name: 'input-group',
|
||||
label: '输入组合校验',
|
||||
body: [
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'child1',
|
||||
label: 'child1',
|
||||
validations: {
|
||||
isNumeric: true,
|
||||
maxLength: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input-text',
|
||||
name: 'child2',
|
||||
validations: {
|
||||
minLength: 5
|
||||
}
|
||||
}
|
||||
],
|
||||
...schema
|
||||
}
|
||||
]
|
||||
},
|
||||
{onSubmit, ...props},
|
||||
makeEnv({...env})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
describe('InputGroup with validationConfig', () => {
|
||||
test('InputGroup with validationConfig: errorMode', async () => {
|
||||
const {container} = setup({validationConfig: {errorMode: 'partial'}});
|
||||
const child1 = container.querySelector(
|
||||
'input[name=child1]'
|
||||
) as HTMLInputElement;
|
||||
const child2 = container.querySelector(
|
||||
'input[name=child2]'
|
||||
) as HTMLInputElement;
|
||||
fireEvent.change(child1, {target: {value: 'amis'}});
|
||||
await wait(300);
|
||||
|
||||
const submitBtn = screen.getByRole('button', {name: 'Submit'});
|
||||
fireEvent.click(submitBtn);
|
||||
await wait(500);
|
||||
|
||||
expect(
|
||||
container.querySelector('*[class*="InputGroup-validation--partial"]')
|
||||
).toBeInTheDocument();
|
||||
|
||||
const errorDom = container.querySelector('*[class*="Form-feedback"]');
|
||||
expect(errorDom?.childElementCount).toStrictEqual(1);
|
||||
|
||||
fireEvent.change(child2, {target: {value: 'amis'}});
|
||||
await wait(300);
|
||||
|
||||
fireEvent.click(submitBtn);
|
||||
await wait(500);
|
||||
|
||||
expect(errorDom?.childElementCount).toStrictEqual(2);
|
||||
});
|
||||
|
||||
test('InputGroup with validationConfig: delimiter', async () => {
|
||||
const delimiter = '@@';
|
||||
const {container} = setup({validationConfig: {delimiter}});
|
||||
const child1 = container.querySelector(
|
||||
'input[name=child1]'
|
||||
) as HTMLInputElement;
|
||||
const child2 = container.querySelector(
|
||||
'input[name=child2]'
|
||||
) as HTMLInputElement;
|
||||
fireEvent.change(child1, {target: {value: 'amis'}});
|
||||
await wait(500);
|
||||
|
||||
const submitBtn = screen.getByRole('button', {name: 'Submit'});
|
||||
fireEvent.click(submitBtn);
|
||||
await wait(500);
|
||||
|
||||
screen.debug(container);
|
||||
|
||||
expect(
|
||||
container.querySelector('*[class*="InputGroup-validation--full"]')
|
||||
).toBeInTheDocument();
|
||||
|
||||
const errorDom = container.querySelector('*[class*="Form-feedback"]');
|
||||
expect(errorDom?.childElementCount).toStrictEqual(1);
|
||||
|
||||
const child1ErrorText = errorDom?.childNodes[0]
|
||||
? errorDom.childNodes[0].textContent
|
||||
: '';
|
||||
|
||||
expect(child1ErrorText).toEqual(expect.stringMatching(delimiter));
|
||||
});
|
||||
});
|
||||
|
@ -1,19 +1,14 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
makeColumnClassBuild,
|
||||
makeHorizontalDeeper,
|
||||
isVisible,
|
||||
isDisabled
|
||||
getExprProperties,
|
||||
FormItem,
|
||||
FormControlProps,
|
||||
IFormItemStore,
|
||||
IFormStore,
|
||||
anyChanged
|
||||
} from 'amis-core';
|
||||
import {getExprProperties} from 'amis-core';
|
||||
import {FormItem, FormControlProps, FormBaseControl} from 'amis-core';
|
||||
import {IFormItemStore, IFormStore} from 'amis-core';
|
||||
import {
|
||||
FormBaseControlSchema,
|
||||
SchemaClassName,
|
||||
SchemaCollection,
|
||||
SchemaObject
|
||||
} from '../../Schema';
|
||||
import {FormBaseControlSchema, SchemaCollection} from '../../Schema';
|
||||
|
||||
/**
|
||||
* InputGroup
|
||||
@ -26,6 +21,21 @@ export interface InputGroupControlSchema extends FormBaseControlSchema {
|
||||
* FormItem 集合
|
||||
*/
|
||||
body: SchemaCollection;
|
||||
|
||||
/**
|
||||
* 校验提示信息配置
|
||||
*/
|
||||
validationConfig?: {
|
||||
/**
|
||||
* 错误提示的展示模式, full为整体飘红, highlight为仅错误项飘红, 默认为full
|
||||
*/
|
||||
errorMode?: 'full' | 'partial';
|
||||
|
||||
/**
|
||||
* 单个子元素多条校验信息的分隔符
|
||||
*/
|
||||
delimiter?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InputGroupProps extends FormControlProps {
|
||||
@ -41,17 +51,90 @@ export class InputGroup extends React.Component<
|
||||
InputGroupProps,
|
||||
InputGroupState
|
||||
> {
|
||||
static defaultProps = {
|
||||
validationConfig: {
|
||||
errorMode: 'full',
|
||||
delimiter: '; '
|
||||
}
|
||||
};
|
||||
|
||||
toDispose: Array<Function> = [];
|
||||
|
||||
constructor(props: InputGroupProps) {
|
||||
super(props);
|
||||
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.validateHook = this.validateHook.bind(this);
|
||||
|
||||
this.state = {
|
||||
isFocused: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {addHook, name} = this.props;
|
||||
|
||||
if (name && addHook) {
|
||||
this.toDispose.push(addHook(this.validateHook, 'validate'));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<InputGroupProps>): void {
|
||||
if (
|
||||
anyChanged(
|
||||
['errorCode', 'delimiter'],
|
||||
prevProps?.validationConfig,
|
||||
this.props?.validationConfig
|
||||
)
|
||||
) {
|
||||
this.validateHook();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.toDispose.forEach(fn => fn());
|
||||
this.toDispose = [];
|
||||
}
|
||||
|
||||
getValidationConfig() {
|
||||
const {validationConfig} = this.props;
|
||||
|
||||
return {
|
||||
errorMode: validationConfig?.errorMode !== 'partial' ? 'full' : 'partial',
|
||||
delimiter:
|
||||
validationConfig?.delimiter &&
|
||||
typeof validationConfig.delimiter === 'string'
|
||||
? validationConfig.delimiter
|
||||
: '; '
|
||||
};
|
||||
}
|
||||
|
||||
validateHook() {
|
||||
const {formStore, formItem, name} = this.props;
|
||||
const {delimiter} = this.getValidationConfig();
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chidren = formStore?.inputGroupItems?.[name];
|
||||
const errorCollection = chidren
|
||||
.map((item, index) => {
|
||||
if (item.errors.length <= 0) {
|
||||
return '';
|
||||
}
|
||||
/** 标识符格式: 索引值 + label */
|
||||
const identifier = item.label
|
||||
? `(${index + 1})${item.label}`
|
||||
: `(${index + 1})`;
|
||||
return `${identifier}: ${item.errors.join(delimiter)}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
formItem && formItem.setError(errorCollection);
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
this.setState({
|
||||
isFocused: true
|
||||
@ -109,6 +192,7 @@ export class InputGroup extends React.Component<
|
||||
static: isStatic,
|
||||
disabled
|
||||
} = this.props;
|
||||
const {errorMode} = this.getValidationConfig();
|
||||
|
||||
formMode = mode || formMode;
|
||||
let inputs: Array<any> = Array.isArray(controls) ? controls : body;
|
||||
@ -136,9 +220,14 @@ export class InputGroup extends React.Component<
|
||||
: undefined);
|
||||
return (
|
||||
<div
|
||||
className={cx(`InputGroup`, className, {
|
||||
'is-focused': this.state.isFocused
|
||||
})}
|
||||
className={cx(
|
||||
`InputGroup`,
|
||||
`InputGroup-validation--${errorMode}`,
|
||||
className,
|
||||
{
|
||||
'is-focused': this.state.isFocused
|
||||
}
|
||||
)}
|
||||
>
|
||||
{inputs.map((control, index) => {
|
||||
const isAddOn = ~[
|
||||
@ -154,6 +243,11 @@ export class InputGroup extends React.Component<
|
||||
formHorizontal: horizontalDeeper,
|
||||
formMode: 'normal',
|
||||
inputOnly: true,
|
||||
inputGroupControl: {
|
||||
name: this.props.name,
|
||||
path: this.props.$path,
|
||||
schema: this.props.$schema
|
||||
},
|
||||
key: index,
|
||||
static: isStatic,
|
||||
disabled,
|
||||
|
Loading…
Reference in New Issue
Block a user