Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions packages/components/form/FormItem.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { flattenDeep, get, isEqual, isFunction, isObject, isString, merge, set, unset } from 'lodash-es';
import React, { forwardRef, ReactNode, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import {
CheckCircleFilledIcon as TdCheckCircleFilledIcon,
CloseCircleFilledIcon as TdCloseCircleFilledIcon,
ErrorCircleFilledIcon as TdErrorCircleFilledIcon,
} from 'tdesign-icons-react';
import { flattenDeep, get, isEqual, isFunction, isObject, isString, merge, set, unset } from 'lodash-es';
import useConfig from '../hooks/useConfig';
import useDefaultProps from '../hooks/useDefaultProps';
import useGlobalIcon from '../hooks/useGlobalIcon';
Expand All @@ -20,11 +20,14 @@ import { calcFieldValue } from './utils';

import type { StyledProps } from '../common';
import type {
FieldData,
FormInstanceFunctions,
FormItemValidateMessage,
FormRule,
NamePath,
TdFormItemProps,
TdFormProps,
ValidateTriggerType,
ValueType,
} from './type';

Expand All @@ -34,18 +37,19 @@ export interface FormItemProps extends TdFormItemProps, StyledProps {

export interface FormItemInstance {
name?: NamePath;
isUpdated?: boolean;
value?: any;
getValue?: Function;
setValue?: Function;
setField?: Function;
validate?: Function;
resetField?: Function;
setValidateMessage?: Function;
getValidateMessage?: Function;
resetValidate?: Function;
validateOnly?: Function;
isUpdated?: boolean;
isFormList?: boolean;
formListMapRef?: React.MutableRefObject<Map<any, any>>;
getValue?: () => any;
setValue?: (newVal: any, originalData?: any) => void;
setField?: (field: Omit<FieldData, 'name'>, originalData?: any) => void;
validate?: (trigger?: ValidateTriggerType, showErrorMessage?: boolean) => Promise<Record<string, any>>;
validateOnly?: (trigger?: ValidateTriggerType) => Promise<Record<string, any>>;
resetField?: (type?: TdFormProps['resetType']) => void;
setValidateMessage?: (message: FormItemValidateMessage[]) => void;
getValidateMessage?: FormInstanceFunctions['getValidateMessage'];
resetValidate?: () => void;
}

const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref) => {
Expand Down Expand Up @@ -110,7 +114,6 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
const [formValue, setFormValue] = useState(() => {
const fieldName = flattenDeep([formListName, name]);
const storeValue = get(form?.store, fieldName);
// if (!storeValue && formListName) return; // TODO 针对新增空的动态表单情况,避免回填默认值
return (
storeValue ??
getDefaultInitialData({
Expand Down Expand Up @@ -227,7 +230,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
return null;
};

async function analysisValidateResult(trigger) {
async function analysisValidateResult(trigger: ValidateTriggerType) {
const result = {
successList: [],
errorList: [],
Expand Down Expand Up @@ -264,7 +267,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
return result;
}

async function validate(trigger = 'all', showErrorMessage?: boolean) {
async function validate(trigger: ValidateTriggerType = 'all', showErrorMessage?: boolean) {
if (innerFormItemsRef.current.length) {
return innerFormItemsRef.current.map((innerFormItem) => innerFormItem?.validate(trigger, showErrorMessage));
}
Expand Down Expand Up @@ -318,7 +321,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
};
}

async function validateOnly(trigger = 'all') {
async function validateOnly(trigger: ValidateTriggerType = 'all') {
const { errorList: innerErrorList, resultList } = await analysisValidateResult(trigger);

return {
Expand All @@ -333,7 +336,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
filterRules.length && validate('blur');
}

function getResetValue(resetType: string): ValueType {
function getResetValue(resetType: TdFormProps['resetType']): ValueType {
if (resetType === 'initial') {
return getDefaultInitialData({
children,
Expand All @@ -353,7 +356,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
return emptyValue;
}

function resetField(type: string) {
function resetField(type: TdFormProps['resetType']) {
if (typeof name === 'undefined') return;

const resetType = type || resetTypeFromContext;
Expand All @@ -375,7 +378,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
setVerifyStatus(ValidateStatus.VALIDATING);
}

function setField(field: { value?: string; status?: ValidateStatus; validateMessage?: FormItemValidateMessage }) {
function setField(field: Omit<FieldData, 'name'>) {
const { value, status, validateMessage } = field;
if (typeof status !== 'undefined') {
setErrorList(validateMessage ? [validateMessage] : []);
Expand Down
11 changes: 6 additions & 5 deletions packages/components/form/FormList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ const FormList: React.FC<TdFormListProps> = (props) => {
(): FormItemInstance => ({
name,
isFormList: true,
formListMapRef,
getValue() {
const formListValue = [];
[...formListMapRef.current.values()].forEach((formItemRef) => {
Expand Down Expand Up @@ -214,26 +215,26 @@ const FormList: React.FC<TdFormListProps> = (props) => {
});
},
// TODO 支持局部更新数据
setValue: (fieldData: any[], originData) => {
setValue: (fieldData, originalData) => {
setListFields(
fieldData,
(formItemRef, data) => {
formItemRef?.current?.setValue?.(data);
},
originData,
originalData,
);
},
setField: (fieldData: { value?: any[]; status?: string }, originData) => {
setField: (fieldData, originalData) => {
const { value, status } = fieldData;
setListFields(
value,
(formItemRef, data) => {
formItemRef?.current?.setField?.({ value: data, status });
},
originData,
originalData,
);
},
resetField: (type: string) => {
resetField: (type) => {
const resetType = type || resetTypeFromContext;

if (resetType === 'initial') {
Expand Down
115 changes: 108 additions & 7 deletions packages/components/form/__tests__/form.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
/* eslint-disable */
import { render, fireEvent, mockDelay, mockTimeout, vi } from '@test/utils';
import React, { useEffect, useState } from 'react';

import Form, { TdFormProps } from '../index';
import Input from '../../input';
import Button from '../../button';
import Radio from '../../radio';
import { HelpCircleIcon } from 'tdesign-icons-react';
import { fireEvent, mockDelay, mockTimeout, render, vi } from '@test/utils';
import Button from '../../button';
import Checkbox from '../../checkbox';
import Input from '../../input';
import InputNumber from '../../input-number';
import { Checkbox } from 'tdesign-react';
import Radio from '../../radio';
import Form, { type TdFormProps } from '../index';

const { FormItem, FormList } = Form;

Expand Down Expand Up @@ -286,6 +285,108 @@ describe('Form 组件测试', () => {
expect(container.querySelector('.password .t-input__extra').innerHTML).toBe('custom warning message');
});

test('Form.getValidateMessage works fine', async () => {
const TestForm = () => {
const [form] = Form.useForm();
const [message, setMessage] = useState<any>(null);

const handleGetAll = () => {
const msg = form.getValidateMessage();
setMessage(msg);
};

const handleGetSpecific = () => {
const msg = form.getValidateMessage(['username', 'email']);
setMessage(msg);
};

const handleValidate = async () => {
await form.validate();
const msg = form.getValidateMessage();
setMessage(msg);
};

return (
<div>
<Form form={form}>
<FormItem
className="username"
label="username"
name="username"
rules={[{ required: true, message: 'username is required' }]}
>
<Input placeholder="username" />
</FormItem>
<FormItem
className="email"
label="email"
name="email"
rules={[{ required: true, message: 'email is required', type: 'error' }]}
>
<Input placeholder="email" />
</FormItem>
<FormItem
className="phone"
label="phone"
name="phone"
rules={[{ required: true, message: 'phone is required', type: 'warning' }]}
>
<Input placeholder="phone" />
</FormItem>
<FormItem
className="nested"
label="nested"
name={['user', 'address']}
rules={[{ required: true, message: 'address is required' }]}
>
<Input placeholder="address" />
</FormItem>
<FormItem>
<Button onClick={handleValidate}>validate</Button>
<Button onClick={handleGetAll}>getAllMessages</Button>
<Button onClick={handleGetSpecific}>getSpecificMessages</Button>
</FormItem>
</Form>
{message && (
<div className="message-display" data-testid="message-display">
<pre>{JSON.stringify(message, null, 2)}</pre>
</div>
)}
</div>
);
};

const { getByText, queryByTestId, getByTestId } = render(<TestForm />);

// getValidateMessage returns undefined when no validation errors
fireEvent.click(getByText('getAllMessages'));
await mockDelay();
expect(queryByTestId('message-display')).toBeNull();

// getValidateMessage returns all validation messages after validate
fireEvent.click(getByText('validate'));
await mockDelay();
fireEvent.click(getByText('getAllMessages'));
await mockDelay();
const allMessagesDisplay = getByTestId('message-display');
expect(allMessagesDisplay.textContent).toContain('username is required');
expect(allMessagesDisplay.textContent).toContain('email is required');
expect(allMessagesDisplay.textContent).toContain('phone is required');
expect(allMessagesDisplay.textContent).toContain('address is required');
// array field name is converted to string key
expect(allMessagesDisplay.textContent).toContain('user,address');

// getValidateMessage returns specific field messages
fireEvent.click(getByText('getSpecificMessages'));
await mockDelay();
const specificMessagesDisplay = getByTestId('message-display');
const messageContent = specificMessagesDisplay.textContent;
expect(messageContent).toContain('username is required');
expect(messageContent).toContain('email is required');
expect(messageContent).not.toContain('phone is required');
expect(messageContent).not.toContain('address is required');
});

test('Form disabled `input` keydown enter submit form', async () => {
const TestForm = () => {
return (
Expand Down
Loading
Loading