mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-29 18:50:00 +08:00
feat: Form support scrollToField
(#17457)
* support scroll & label htmlFor * update snapshot * add test case * doc update * add decleare * adjust logic of label id * clean ts error
This commit is contained in:
parent
186f973e50
commit
b3a76c14ca
@ -1,16 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import omit from 'omit.js';
|
||||
import classNames from 'classnames';
|
||||
import FieldForm, { FormInstance, useForm, List } from 'rc-field-form';
|
||||
import FieldForm, { List } from 'rc-field-form';
|
||||
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
|
||||
import { ColProps } from '../grid/col';
|
||||
import { ConfigContext, ConfigConsumerProps } from '../config-provider';
|
||||
import { FormContext } from './context';
|
||||
import { FormLabelAlign } from './interface';
|
||||
import { useForm, FormInstance } from './util';
|
||||
|
||||
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
|
||||
|
||||
interface FormProps extends RcFormProps {
|
||||
interface FormProps extends Omit<RcFormProps, 'form'> {
|
||||
prefixCls?: string;
|
||||
hideRequiredMark?: boolean;
|
||||
colon?: boolean;
|
||||
@ -19,12 +20,14 @@ interface FormProps extends RcFormProps {
|
||||
labelAlign?: FormLabelAlign;
|
||||
labelCol?: ColProps;
|
||||
wrapperCol?: ColProps;
|
||||
form?: FormInstance;
|
||||
}
|
||||
|
||||
const InternalForm: React.FC<FormProps> = (props, ref) => {
|
||||
const { getPrefixCls }: ConfigConsumerProps = React.useContext(ConfigContext);
|
||||
|
||||
const {
|
||||
form,
|
||||
colon,
|
||||
name,
|
||||
labelAlign,
|
||||
@ -57,6 +60,11 @@ const InternalForm: React.FC<FormProps> = (props, ref) => {
|
||||
'colon',
|
||||
]);
|
||||
|
||||
const [wrapForm] = useForm(form);
|
||||
wrapForm.__INTERNAL__.name = name;
|
||||
|
||||
React.useImperativeHandle(ref, () => wrapForm);
|
||||
|
||||
return (
|
||||
<FormContext.Provider
|
||||
value={{
|
||||
@ -68,7 +76,7 @@ const InternalForm: React.FC<FormProps> = (props, ref) => {
|
||||
colon,
|
||||
}}
|
||||
>
|
||||
<FieldForm id={name} {...formProps} ref={ref} className={formClassName} />
|
||||
<FieldForm id={name} {...formProps} form={wrapForm} className={formClassName} />
|
||||
</FormContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ import warning from '../_util/warning';
|
||||
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel';
|
||||
import FormItemInput, { FormItemInputProps } from './FormItemInput';
|
||||
import { FormContext, FormItemContext } from './context';
|
||||
import { toArray } from './util';
|
||||
import { toArray, getFieldId } from './util';
|
||||
|
||||
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
|
||||
export type ValidateStatus = (typeof ValidateStatuses)[number];
|
||||
@ -154,10 +154,10 @@ const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
||||
: !!(rules && rules.some(rule => typeof rule === 'object' && rule.required));
|
||||
|
||||
// ======================= Children =======================
|
||||
const mergedId = mergedName.join('_');
|
||||
const fieldId = getFieldId(mergedName, formName);
|
||||
const mergedControl: typeof control = {
|
||||
...control,
|
||||
id: formName ? `${formName}_${mergedId}` : mergedId,
|
||||
id: fieldId,
|
||||
};
|
||||
|
||||
let childNode;
|
||||
@ -195,7 +195,12 @@ const FormItem: React.FC<FormItemProps> = (props: FormItemProps) => {
|
||||
return (
|
||||
<Row type="flex" className={classNames(itemClassName)} style={style} key="row">
|
||||
{/* Label */}
|
||||
<FormItemLabel {...props} required={isRequired} prefixCls={prefixCls} />
|
||||
<FormItemLabel
|
||||
htmlFor={fieldId}
|
||||
{...props}
|
||||
required={isRequired}
|
||||
prefixCls={prefixCls}
|
||||
/>
|
||||
{/* Input Group */}
|
||||
<FormItemInput
|
||||
{...props}
|
||||
|
@ -6,7 +6,7 @@ import { FormContext, FormContextProps } from './context';
|
||||
|
||||
export interface FormItemLabelProps {
|
||||
colon?: boolean;
|
||||
htmlFor: string;
|
||||
htmlFor?: string;
|
||||
label?: React.ReactNode;
|
||||
labelAlign?: FormLabelAlign;
|
||||
labelCol?: ColProps;
|
||||
|
@ -22,6 +22,7 @@ exports[`renders ./components/form/demo/advanced-search.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="advanced_search_field-0"
|
||||
title="Field 0"
|
||||
>
|
||||
Field 0
|
||||
@ -56,6 +57,7 @@ exports[`renders ./components/form/demo/advanced-search.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="advanced_search_field-1"
|
||||
title="Field 1"
|
||||
>
|
||||
Field 1
|
||||
@ -90,6 +92,7 @@ exports[`renders ./components/form/demo/advanced-search.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="advanced_search_field-2"
|
||||
title="Field 2"
|
||||
>
|
||||
Field 2
|
||||
@ -124,6 +127,7 @@ exports[`renders ./components/form/demo/advanced-search.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="advanced_search_field-3"
|
||||
title="Field 3"
|
||||
>
|
||||
Field 3
|
||||
@ -158,6 +162,7 @@ exports[`renders ./components/form/demo/advanced-search.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="advanced_search_field-4"
|
||||
title="Field 4"
|
||||
>
|
||||
Field 4
|
||||
@ -192,6 +197,7 @@ exports[`renders ./components/form/demo/advanced-search.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="advanced_search_field-5"
|
||||
title="Field 5"
|
||||
>
|
||||
Field 5
|
||||
@ -287,6 +293,7 @@ exports[`renders ./components/form/demo/basic.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="basic_username"
|
||||
title="Username"
|
||||
>
|
||||
Username
|
||||
@ -315,6 +322,7 @@ exports[`renders ./components/form/demo/basic.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="basic_password"
|
||||
title="Password"
|
||||
>
|
||||
Password
|
||||
@ -435,6 +443,7 @@ exports[`renders ./components/form/demo/control-hooks.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="control-hooks_note"
|
||||
title="Note"
|
||||
>
|
||||
Note
|
||||
@ -463,6 +472,7 @@ exports[`renders ./components/form/demo/control-hooks.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="control-hooks_gender"
|
||||
title="Gender"
|
||||
>
|
||||
Gender
|
||||
@ -581,6 +591,7 @@ exports[`renders ./components/form/demo/control-ref.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="control-ref_note"
|
||||
title="Note"
|
||||
>
|
||||
Note
|
||||
@ -609,6 +620,7 @@ exports[`renders ./components/form/demo/control-ref.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="control-ref_gender"
|
||||
title="Gender"
|
||||
>
|
||||
Gender
|
||||
@ -727,6 +739,7 @@ exports[`renders ./components/form/demo/customized-form-controls.md correctly 1`
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="customized_form_controls_price"
|
||||
title="Price"
|
||||
>
|
||||
Price
|
||||
@ -910,6 +923,7 @@ exports[`renders ./components/form/demo/dynamic-rule.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="dynamic_rule_username"
|
||||
title="Name"
|
||||
>
|
||||
Name
|
||||
@ -939,6 +953,7 @@ exports[`renders ./components/form/demo/dynamic-rule.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="dynamic_rule_nickname"
|
||||
title="Nickname"
|
||||
>
|
||||
Nickname
|
||||
@ -1027,6 +1042,7 @@ exports[`renders ./components/form/demo/form-context.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="basicForm_group"
|
||||
title="Group Name"
|
||||
>
|
||||
Group Name
|
||||
@ -1154,6 +1170,7 @@ exports[`renders ./components/form/demo/global-state.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="global_state_username"
|
||||
title="Username"
|
||||
>
|
||||
Username
|
||||
@ -1494,6 +1511,7 @@ exports[`renders ./components/form/demo/nest-messages.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="nest-messages_user_name"
|
||||
title="Name"
|
||||
>
|
||||
Name
|
||||
@ -1522,6 +1540,7 @@ exports[`renders ./components/form/demo/nest-messages.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="nest-messages_user_email"
|
||||
title="Email"
|
||||
>
|
||||
Email
|
||||
@ -1550,6 +1569,7 @@ exports[`renders ./components/form/demo/nest-messages.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="nest-messages_user_age"
|
||||
title="Age"
|
||||
>
|
||||
Age
|
||||
@ -1648,6 +1668,7 @@ exports[`renders ./components/form/demo/nest-messages.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="nest-messages_user_website"
|
||||
title="Website"
|
||||
>
|
||||
Website
|
||||
@ -1676,6 +1697,7 @@ exports[`renders ./components/form/demo/nest-messages.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="nest-messages_user_introduction"
|
||||
title="Introduction"
|
||||
>
|
||||
Introduction
|
||||
@ -1896,6 +1918,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="register_email"
|
||||
title="E-mail"
|
||||
>
|
||||
E-mail
|
||||
@ -1924,6 +1947,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="register_password"
|
||||
title="Password"
|
||||
>
|
||||
Password
|
||||
@ -1984,6 +2008,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="register_confirm"
|
||||
title="Confirm Password"
|
||||
>
|
||||
Confirm Password
|
||||
@ -2044,6 +2069,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="register_nickname"
|
||||
title=""
|
||||
>
|
||||
<span>
|
||||
@ -2096,6 +2122,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="register_residence"
|
||||
title="Habitual Residence"
|
||||
>
|
||||
Habitual Residence
|
||||
@ -2176,6 +2203,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="register_phone"
|
||||
title="Phone Number"
|
||||
>
|
||||
Phone Number
|
||||
@ -2270,6 +2298,7 @@ exports[`renders ./components/form/demo/register.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="register_website"
|
||||
title="Website"
|
||||
>
|
||||
Website
|
||||
@ -2483,6 +2512,7 @@ exports[`renders ./components/form/demo/time-related-controls.md correctly 1`] =
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="time_related_controls_date-picker"
|
||||
title="DatePicker"
|
||||
>
|
||||
DatePicker
|
||||
@ -2537,6 +2567,7 @@ exports[`renders ./components/form/demo/time-related-controls.md correctly 1`] =
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="time_related_controls_date-time-picker"
|
||||
title="DatePicker[showTime]"
|
||||
>
|
||||
DatePicker[showTime]
|
||||
@ -2592,6 +2623,7 @@ exports[`renders ./components/form/demo/time-related-controls.md correctly 1`] =
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="time_related_controls_month-picker"
|
||||
title="MonthPicker"
|
||||
>
|
||||
MonthPicker
|
||||
@ -2646,6 +2678,7 @@ exports[`renders ./components/form/demo/time-related-controls.md correctly 1`] =
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="time_related_controls_range-picker"
|
||||
title="RangePicker"
|
||||
>
|
||||
RangePicker
|
||||
@ -2716,6 +2749,7 @@ exports[`renders ./components/form/demo/time-related-controls.md correctly 1`] =
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="time_related_controls_range-time-picker"
|
||||
title="RangePicker[showTime]"
|
||||
>
|
||||
RangePicker[showTime]
|
||||
@ -2787,6 +2821,7 @@ exports[`renders ./components/form/demo/time-related-controls.md correctly 1`] =
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="time_related_controls_time-picker"
|
||||
title="TimePicker"
|
||||
>
|
||||
TimePicker
|
||||
@ -2901,6 +2936,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="validate_other_select"
|
||||
title="Select"
|
||||
>
|
||||
Select
|
||||
@ -2975,6 +3011,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class="ant-form-item-required"
|
||||
for="validate_other_select-multiple"
|
||||
title="Select[multiple]"
|
||||
>
|
||||
Select[multiple]
|
||||
@ -3150,6 +3187,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="validate_other_switch"
|
||||
title="Switch"
|
||||
>
|
||||
Switch
|
||||
@ -3183,6 +3221,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="validate_other_slider"
|
||||
title="Slider"
|
||||
>
|
||||
Slider
|
||||
@ -3294,6 +3333,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="validate_other_radio-group"
|
||||
title="Radio.Group"
|
||||
>
|
||||
Radio.Group
|
||||
@ -3378,6 +3418,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="validate_other_radio-button"
|
||||
title="Radio.Button"
|
||||
>
|
||||
Radio.Button
|
||||
@ -3462,6 +3503,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="validate_other_checkbox-group"
|
||||
title="Checkbox.Group"
|
||||
>
|
||||
Checkbox.Group
|
||||
@ -3612,6 +3654,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="validate_other_rate"
|
||||
title="Rate"
|
||||
>
|
||||
Rate
|
||||
@ -3930,6 +3973,7 @@ exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="validate_other_upload"
|
||||
title="Upload"
|
||||
>
|
||||
Upload
|
||||
|
@ -1,15 +1,19 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import scrollIntoView from 'dom-scroll-into-view';
|
||||
import Form from '..';
|
||||
import Input from '../../input';
|
||||
import Button from '../../button';
|
||||
|
||||
jest.mock('dom-scroll-into-view');
|
||||
|
||||
const delay = () =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
|
||||
describe('Form', () => {
|
||||
scrollIntoView.mockImplementation(() => {});
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
async function change(wrapper, index, value) {
|
||||
@ -21,12 +25,17 @@ describe('Form', () => {
|
||||
wrapper.update();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
scrollIntoView.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
errorSpy.mockReset();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
errorSpy.mockRestore();
|
||||
scrollIntoView.mockRestore();
|
||||
});
|
||||
|
||||
describe('List', () => {
|
||||
@ -123,4 +132,53 @@ describe('Form', () => {
|
||||
'Warning: [antd: Form.Item] `children` of render props only work with `shouldUpdate`.',
|
||||
);
|
||||
});
|
||||
|
||||
describe('scrollToField', () => {
|
||||
function test(name, genForm) {
|
||||
it(name, () => {
|
||||
let callGetForm;
|
||||
|
||||
const Demo = () => {
|
||||
const { props, getForm } = genForm();
|
||||
callGetForm = getForm;
|
||||
|
||||
return (
|
||||
<Form name="scroll" {...props}>
|
||||
<Form.Item name="test">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
mount(<Demo />, { attachTo: document.body });
|
||||
|
||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||
callGetForm().scrollToField('test');
|
||||
expect(scrollIntoView).toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
|
||||
// hooks
|
||||
test('useForm', () => {
|
||||
const [form] = Form.useForm();
|
||||
return {
|
||||
props: { form },
|
||||
getForm: () => form,
|
||||
};
|
||||
});
|
||||
|
||||
// ref
|
||||
test('ref', () => {
|
||||
let form;
|
||||
return {
|
||||
props: {
|
||||
ref: instance => {
|
||||
form = instance;
|
||||
},
|
||||
},
|
||||
getForm: () => form,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -90,10 +90,16 @@ const tailFormItemLayout = {
|
||||
};
|
||||
|
||||
const RegistrationForm = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = values => {
|
||||
console.log('Received values of form: ', values);
|
||||
};
|
||||
|
||||
const onFinishFailed = ({ errorFields }) => {
|
||||
form.scrollToField(errorFields[0].name);
|
||||
};
|
||||
|
||||
const prefixSelector = (
|
||||
<Form.Item name="prefix" inline>
|
||||
<Select style={{ width: 70 }}>
|
||||
@ -120,8 +126,10 @@ const RegistrationForm = () => {
|
||||
return (
|
||||
<Form
|
||||
{...formItemLayout}
|
||||
form={form}
|
||||
name="register"
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
initialValues={{
|
||||
residence: ['zhejiang', 'hangzhou', 'xihu'],
|
||||
prefix: '86',
|
||||
|
@ -181,6 +181,7 @@ Provide linkage between forms. If a sub form with `name` prop update, it will au
|
||||
| isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
|
||||
| isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean |
|
||||
| resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
|
||||
| scrollToField | Scroll to field position | (name: [NamePath](#NamePath)) => void |
|
||||
| setFields | Set fields status | (fields: FieldData[]) => void |
|
||||
| setFieldsValue | Set fields value | (values) => void |
|
||||
| submit | Submit the form. It's same as click `submit` button | () => void |
|
||||
|
@ -182,6 +182,7 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
|
||||
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
|
||||
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean |
|
||||
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
|
||||
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath)) => void |
|
||||
| setFields | 设置一组字段状态 | (fields: FieldData[]) => void |
|
||||
| setFieldsValue | 设置表单的值 | (values) => void |
|
||||
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void |
|
||||
|
@ -1,4 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { useForm as useRcForm, FormInstance as RcFormInstance } from 'rc-field-form';
|
||||
import scrollIntoView from 'dom-scroll-into-view';
|
||||
|
||||
type InternalNamePath = (string | number)[];
|
||||
|
||||
/**
|
||||
* We will remove light way shake like:
|
||||
@ -58,3 +62,59 @@ export function toArray<T>(candidate?: T | T[] | false): T[] {
|
||||
|
||||
return Array.isArray(candidate) ? candidate : [candidate];
|
||||
}
|
||||
|
||||
export function getFieldId(namePath: InternalNamePath, formName?: string): string | undefined {
|
||||
if (!namePath.length) return undefined;
|
||||
|
||||
const mergedId = namePath.join('_');
|
||||
return formName ? `${formName}_${mergedId}` : mergedId;
|
||||
}
|
||||
|
||||
// Source: https://github.com/react-component/form/blob/master/src/createDOMForm.js
|
||||
function getScrollableContainer(current: HTMLElement) {
|
||||
let node: HTMLElement | null = current;
|
||||
let nodeName;
|
||||
/* eslint no-cond-assign:0 */
|
||||
while (node && (nodeName = node.nodeName.toLowerCase()) !== 'body') {
|
||||
const { overflowY } = getComputedStyle(node);
|
||||
// https://stackoverflow.com/a/36900407/3040605
|
||||
if (
|
||||
node !== current &&
|
||||
(overflowY === 'auto' || overflowY === 'scroll') &&
|
||||
node.scrollHeight > node.clientHeight
|
||||
) {
|
||||
return node;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
return nodeName === 'body' ? node!.ownerDocument : node;
|
||||
}
|
||||
|
||||
export interface FormInstance extends RcFormInstance {
|
||||
scrollToField: (name: string | number | InternalNamePath) => void;
|
||||
__INTERNAL__: {
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useForm(form?: FormInstance): [FormInstance] {
|
||||
const wrapForm: FormInstance = form
|
||||
? form
|
||||
: {
|
||||
...useRcForm()[0],
|
||||
__INTERNAL__: {},
|
||||
scrollToField: name => {
|
||||
const namePath = toArray(name);
|
||||
const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
|
||||
const node: HTMLElement | null = fieldId ? document.getElementById(fieldId) : null;
|
||||
|
||||
if (node) {
|
||||
scrollIntoView(node, getScrollableContainer(node), {
|
||||
onlyScrollIfNeeded: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return [wrapForm];
|
||||
}
|
||||
|
@ -191,6 +191,28 @@ const Demo = () => {
|
||||
};
|
||||
```
|
||||
|
||||
## Replace validateFieldsAndScroll with scrollToField
|
||||
|
||||
New version recommend use `onFinish` for submit after validation. Thus `validateFieldsAndScroll` is change to more flexible method `scrollToField`:
|
||||
|
||||
```jsx
|
||||
// antd v3
|
||||
onSubmit = () => {
|
||||
form.validateFieldsAndScroll((error, values) => {
|
||||
// Your logic
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
To:
|
||||
|
||||
```jsx
|
||||
// antd v4
|
||||
onFinishFailed = ({ errorFields }) => {
|
||||
form.scrollToField(errorFields[0].name);
|
||||
};
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
Besides, we move `initialValue` into Form to avoid field with same name both using `initialValue` to cause conflict:
|
||||
|
@ -193,6 +193,28 @@ const Demo = () => {
|
||||
};
|
||||
```
|
||||
|
||||
## scrollToField 替代 validateFieldsAndScroll
|
||||
|
||||
新版推荐使用 `onFinish` 进行校验后提交操作,因而 `validateFieldsAndScroll` 拆成更独立的 `scrollToField` 方法:
|
||||
|
||||
```jsx
|
||||
// antd v3
|
||||
onSubmit = () => {
|
||||
form.validateFieldsAndScroll((error, values) => {
|
||||
// Your logic
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
改成:
|
||||
|
||||
```jsx
|
||||
// antd v4
|
||||
onFinishFailed = ({ errorFields }) => {
|
||||
form.scrollToField(errorFields[0].name);
|
||||
};
|
||||
```
|
||||
|
||||
## 初始化调整
|
||||
|
||||
此外,我们将 `initialValue` 从字段中移到 Form 中。以避免同名字段设置 `initialValue` 的冲突问题:
|
||||
|
@ -261,6 +261,7 @@ exports[`renders ./components/mention/demo/controlled.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="mention"
|
||||
title="Top coders"
|
||||
>
|
||||
Top coders
|
||||
|
@ -36,6 +36,7 @@ exports[`renders ./components/mentions/demo/form.md correctly 1`] = `
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="mention"
|
||||
title="Top coders"
|
||||
>
|
||||
Top coders
|
||||
|
@ -52,6 +52,7 @@
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"css-animation": "^1.5.0",
|
||||
"dom-closest": "^0.2.0",
|
||||
"dom-scroll-into-view": "^1.2.1",
|
||||
"enquire.js": "^2.1.6",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.24.0",
|
||||
|
2
typings/custom-typings.d.ts
vendored
2
typings/custom-typings.d.ts
vendored
@ -83,6 +83,8 @@ declare module 'react-lazy-load';
|
||||
|
||||
declare module 'dom-closest';
|
||||
|
||||
declare module 'dom-scroll-into-view';
|
||||
|
||||
declare module '*.json' {
|
||||
const value: any;
|
||||
export const version: string;
|
||||
|
Loading…
Reference in New Issue
Block a user