feat: form instance support getFieldInstance (#24711)

* support getFieldInstance

* update doc

* fix lint

* move func

* move into hooks

* update ref logic

* fix lint

* rm only

* fix docs
This commit is contained in:
二货机器人 2020-06-05 18:06:52 +08:00 committed by GitHub
parent c2beed8bd3
commit e46d414b11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 480 additions and 192 deletions

View File

@ -1,5 +1,4 @@
import * as React from 'react'; import * as React from 'react';
import omit from 'omit.js';
import classNames from 'classnames'; import classNames from 'classnames';
import FieldForm, { List } from 'rc-field-form'; import FieldForm, { List } from 'rc-field-form';
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form'; import { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
@ -8,7 +7,7 @@ import { ColProps } from '../grid/col';
import { ConfigContext, ConfigConsumerProps } from '../config-provider'; import { ConfigContext, ConfigConsumerProps } from '../config-provider';
import { FormContext } from './context'; import { FormContext } from './context';
import { FormLabelAlign } from './interface'; import { FormLabelAlign } from './interface';
import { useForm, FormInstance } from './util'; import useForm, { FormInstance } from './hooks/useForm';
import SizeContext, { SizeType, SizeContextProvider } from '../config-provider/SizeContext'; import SizeContext, { SizeType, SizeContextProvider } from '../config-provider/SizeContext';
export type FormLayout = 'horizontal' | 'inline' | 'vertical'; export type FormLayout = 'horizontal' | 'inline' | 'vertical';
@ -31,21 +30,24 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
const contextSize = React.useContext(SizeContext); const contextSize = React.useContext(SizeContext);
const { getPrefixCls, direction }: ConfigConsumerProps = React.useContext(ConfigContext); const { getPrefixCls, direction }: ConfigConsumerProps = React.useContext(ConfigContext);
const { name } = props;
const { const {
prefixCls: customizePrefixCls,
className = '',
size = contextSize,
form, form,
colon, colon,
name,
labelAlign, labelAlign,
labelCol, labelCol,
wrapperCol, wrapperCol,
prefixCls: customizePrefixCls,
hideRequiredMark, hideRequiredMark,
className = '',
layout = 'horizontal', layout = 'horizontal',
size = contextSize,
scrollToFirstError, scrollToFirstError,
onFinishFailed, onFinishFailed,
...restFormProps
} = props; } = props;
const prefixCls = getPrefixCls('form', customizePrefixCls); const prefixCls = getPrefixCls('form', customizePrefixCls);
const formClassName = classNames( const formClassName = classNames(
@ -59,20 +61,9 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
className, className,
); );
const formProps = omit(props, [
'prefixCls',
'className',
'layout',
'hideRequiredMark',
'wrapperCol',
'labelAlign',
'labelCol',
'colon',
'scrollToFirstError',
]);
const [wrapForm] = useForm(form); const [wrapForm] = useForm(form);
wrapForm.__INTERNAL__.name = name; const { __INTERNAL__ } = wrapForm;
__INTERNAL__.name = name;
const formContextValue = React.useMemo( const formContextValue = React.useMemo(
() => ({ () => ({
@ -82,6 +73,7 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
wrapperCol, wrapperCol,
vertical: layout === 'vertical', vertical: layout === 'vertical',
colon, colon,
itemRef: __INTERNAL__.itemRef,
}), }),
[name, labelAlign, labelCol, wrapperCol, layout, colon], [name, labelAlign, labelCol, wrapperCol, layout, colon],
); );
@ -100,12 +92,10 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
return ( return (
<SizeContextProvider size={size}> <SizeContextProvider size={size}>
<FormContext.Provider <FormContext.Provider value={formContextValue}>
value={formContextValue}
>
<FieldForm <FieldForm
id={name} id={name}
{...formProps} {...restFormProps}
onFinishFailed={onInternalFinishFailed} onFinishFailed={onInternalFinishFailed}
form={wrapForm} form={wrapForm}
className={formClassName} className={formClassName}

View File

@ -4,6 +4,7 @@ import classNames from 'classnames';
import { Field, FormInstance } from 'rc-field-form'; import { Field, FormInstance } from 'rc-field-form';
import { FieldProps } from 'rc-field-form/lib/Field'; import { FieldProps } from 'rc-field-form/lib/Field';
import { Meta, NamePath } from 'rc-field-form/lib/interface'; import { Meta, NamePath } from 'rc-field-form/lib/interface';
import { supportRef } from 'rc-util/lib/ref';
import omit from 'omit.js'; import omit from 'omit.js';
import Row from '../grid/row'; import Row from '../grid/row';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
@ -12,8 +13,10 @@ import devWarning from '../_util/devWarning';
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel'; import FormItemLabel, { FormItemLabelProps } from './FormItemLabel';
import FormItemInput, { FormItemInputProps } from './FormItemInput'; import FormItemInput, { FormItemInputProps } from './FormItemInput';
import { FormContext, FormItemContext } from './context'; import { FormContext, FormItemContext } from './context';
import { toArray, getFieldId, useFrameState } from './util'; import { toArray, getFieldId } from './util';
import { cloneElement, isValidElement } from '../_util/reactNode'; import { cloneElement, isValidElement } from '../_util/reactNode';
import useFrameState from './hooks/useFrameState';
import useItemRef from './hooks/useItemRef';
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
export type ValidateStatus = typeof ValidateStatuses[number]; export type ValidateStatus = typeof ValidateStatuses[number];
@ -80,7 +83,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
} = props; } = props;
const destroyRef = React.useRef(false); const destroyRef = React.useRef(false);
const { getPrefixCls } = React.useContext(ConfigContext); const { getPrefixCls } = React.useContext(ConfigContext);
const formContext = React.useContext(FormContext); const { name: formName } = React.useContext(FormContext);
const { updateItemErrors } = React.useContext(FormItemContext); const { updateItemErrors } = React.useContext(FormItemContext);
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help); const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
const prevValidateStatusRef = React.useRef<ValidateStatus | undefined>(validateStatus); const prevValidateStatusRef = React.useRef<ValidateStatus | undefined>(validateStatus);
@ -92,7 +95,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
} }
} }
const { name: formName } = formContext;
const hasName = hasValidName(name); const hasName = hasValidName(name);
// Cache Field NamePath // Cache Field NamePath
@ -121,6 +123,9 @@ function FormItem(props: FormItemProps): React.ReactElement {
} }
}; };
// ===================== Children Ref =====================
const getItemRef = useItemRef();
function renderLayout( function renderLayout(
baseChildren: React.ReactNode, baseChildren: React.ReactNode,
fieldId?: string, fieldId?: string,
@ -316,6 +321,10 @@ function FormItem(props: FormItemProps): React.ReactElement {
const childProps = { ...children.props, ...mergedControl }; const childProps = { ...children.props, ...mergedControl };
if (supportRef(children)) {
childProps.ref = getItemRef(mergedName, children);
}
// We should keep user origin event handler // We should keep user origin event handler
const triggers = new Set<string>([...toArray(trigger), ...toArray(validateTrigger)]); const triggers = new Set<string>([...toArray(trigger), ...toArray(validateTrigger)]);

View File

@ -10,7 +10,7 @@ import CSSMotion from 'rc-animate/lib/CSSMotion';
import Col, { ColProps } from '../grid/col'; import Col, { ColProps } from '../grid/col';
import { ValidateStatus } from './FormItem'; import { ValidateStatus } from './FormItem';
import { FormContext } from './context'; import { FormContext } from './context';
import { useCacheErrors } from './util'; import useCacheErrors from './hooks/useCacheErrors';
interface FormItemInputMiscProps { interface FormItemInputMiscProps {
prefixCls: string; prefixCls: string;

View File

@ -2196,6 +2196,84 @@ exports[`renders ./components/form/demo/normal-login.md correctly 1`] = `
</form> </form>
`; `;
exports[`renders ./components/form/demo/ref-item.md correctly 1`] = `
<form
class="ant-form ant-form-horizontal"
>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-label"
>
<label
class=""
for="test"
title="test"
>
test
</label>
</div>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="test"
type="text"
value=""
/>
</div>
</div>
</div>
</div>
<div
class="ant-row ant-form-item"
>
<div
class="ant-col ant-form-item-control"
>
<div
class="ant-form-item-control-input"
>
<div
class="ant-form-item-control-input-content"
>
<input
class="ant-input"
id="list_0"
type="text"
value="light"
/>
</div>
</div>
</div>
</div>
<button
class="ant-btn ant-btn-button"
type="button"
>
<span>
Focus Form.Item
</span>
</button>
<button
class="ant-btn"
type="button"
>
<span>
Focus Form.List
</span>
</button>
</form>
`;
exports[`renders ./components/form/demo/register.md correctly 1`] = ` exports[`renders ./components/form/demo/register.md correctly 1`] = `
<form <form
class="ant-form ant-form-horizontal" class="ant-form ant-form-horizontal"

View File

@ -0,0 +1,91 @@
/* eslint-disable react/jsx-key */
import React from 'react';
import { mount } from 'enzyme';
import Form from '..';
import Input from '../../input';
import Button from '../../button';
describe('Form.Ref', () => {
const Test = ({
onRef,
show,
}: {
onRef: (node: React.ReactElement, originRef: React.RefObject<any>) => void;
show?: boolean;
}) => {
const [form] = Form.useForm();
const removeRef = React.useRef<any>();
const testRef = React.useRef<any>();
const listRef = React.useRef<any>();
return (
<Form form={form} initialValues={{ list: ['light'] }}>
{show && (
<Form.Item name="remove" label="remove">
<Input ref={removeRef} />
</Form.Item>
)}
<Form.Item name="test" label="test">
<Input ref={testRef} />
</Form.Item>
<Form.List name="list">
{fields =>
fields.map(field => (
<Form.Item {...field}>
<Input ref={listRef} />
</Form.Item>
))
}
</Form.List>
<Button
className="ref-item"
onClick={() => {
onRef(form.getFieldInstance('test'), testRef.current);
}}
>
Form.Item
</Button>
<Button
className="ref-list"
onClick={() => {
onRef(form.getFieldInstance(['list', 0]), listRef.current);
}}
>
Form.List
</Button>
<Button
className="ref-remove"
onClick={() => {
onRef(form.getFieldInstance('remove'), removeRef.current);
}}
>
Removed
</Button>
</Form>
);
};
it('should ref work', () => {
const onRef = jest.fn();
const wrapper = mount(<Test onRef={onRef} show />);
wrapper.find('.ref-item').last().simulate('click');
expect(onRef).toHaveBeenCalled();
expect(onRef.mock.calls[0][0]).toBe(onRef.mock.calls[0][1]);
onRef.mockReset();
wrapper.find('.ref-list').last().simulate('click');
expect(onRef).toHaveBeenCalled();
expect(onRef.mock.calls[0][0]).toBe(onRef.mock.calls[0][1]);
onRef.mockReset();
wrapper.setProps({ show: false });
wrapper.update();
wrapper.find('.ref-remove').last().simulate('click');
expect(onRef).toHaveBeenCalledWith(undefined, null);
});
});

View File

@ -16,11 +16,13 @@ export interface FormContextProps {
labelAlign?: FormLabelAlign; labelAlign?: FormLabelAlign;
labelCol?: ColProps; labelCol?: ColProps;
wrapperCol?: ColProps; wrapperCol?: ColProps;
itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
} }
export const FormContext = React.createContext<FormContextProps>({ export const FormContext = React.createContext<FormContextProps>({
labelAlign: 'right', labelAlign: 'right',
vertical: false, vertical: false,
itemRef: (() => {}) as any,
}); });
/** /**

View File

@ -0,0 +1,61 @@
---
order: 999999
title:
zh-CN: 引用字段
en-US: Ref item
debug: true
---
## zh-CN
请优先使用 `ref`
## en-US
Use `ref` first!
```jsx
import React from 'react';
import { Button, Form, Input } from 'antd';
const Demo = () => {
const [form] = Form.useForm();
const ref = React.useRef();
return (
<Form form={form} initialValues={{ list: ['light'] }}>
<Form.Item name="test" label="test">
<Input ref={ref} />
</Form.Item>
<Form.List name="list">
{fields =>
fields.map(field => (
<Form.Item key={field.key} {...field}>
<Input ref={ref} />
</Form.Item>
))
}
</Form.List>
<Button
type="button"
onClick={() => {
form.getFieldInstance('test').focus();
}}
>
Focus Form.Item
</Button>
<Button
onClick={() => {
form.getFieldInstance(['list', 0]).focus();
}}
>
Focus Form.List
</Button>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);
```

View File

@ -0,0 +1,48 @@
import * as React from 'react';
/**
* Always debounce error to avoid [error -> null -> error] blink
*/
export default function useCacheErrors(
errors: React.ReactNode[],
changeTrigger: (visible: boolean) => void,
directly: boolean,
): [boolean, React.ReactNode[]] {
const cacheRef = React.useRef({
errors,
visible: !!errors.length,
});
const [, forceUpdate] = React.useState({});
const update = () => {
const prevVisible = cacheRef.current.visible;
const newVisible = !!errors.length;
const prevErrors = cacheRef.current.errors;
cacheRef.current.errors = errors;
cacheRef.current.visible = newVisible;
if (prevVisible !== newVisible) {
changeTrigger(newVisible);
} else if (
prevErrors.length !== errors.length ||
prevErrors.some((prevErr, index) => prevErr !== errors[index])
) {
forceUpdate({});
}
};
React.useEffect(() => {
if (!directly) {
const timeout = setTimeout(update, 10);
return () => clearTimeout(timeout);
}
}, [errors]);
if (directly) {
update();
}
return [cacheRef.current.visible, cacheRef.current.errors];
}

View File

@ -0,0 +1,64 @@
import { useRef, useMemo } from 'react';
import { useForm as useRcForm, FormInstance as RcFormInstance } from 'rc-field-form';
import scrollIntoView from 'scroll-into-view-if-needed';
import { ScrollOptions, NamePath, InternalNamePath } from '../interface';
import { toArray, getFieldId } from '../util';
export interface FormInstance extends RcFormInstance {
scrollToField: (name: NamePath, options?: ScrollOptions) => void;
/** This is an internal usage. Do not use in your prod */
__INTERNAL__: {
/** No! Do not use this in your code! */
name?: string;
/** No! Do not use this in your code! */
itemRef: (name: InternalNamePath) => (node: React.ReactElement) => void;
};
getFieldInstance: (name: NamePath) => any;
}
function toNamePathStr(name: NamePath) {
const namePath = toArray(name);
return namePath.join('_');
}
export default function useForm(form?: FormInstance): [FormInstance] {
const [rcForm] = useRcForm();
const itemsRef = useRef<Record<string, React.ReactElement>>({});
const wrapForm: FormInstance = useMemo(
() =>
form || {
...rcForm,
__INTERNAL__: {
itemRef: (name: InternalNamePath) => (node: React.ReactElement) => {
const namePathStr = toNamePathStr(name);
if (node) {
itemsRef.current[namePathStr] = node;
} else {
delete itemsRef.current[namePathStr];
}
},
},
scrollToField: (name: string, options: ScrollOptions = {}) => {
const namePath = toArray(name);
const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
const node: HTMLElement | null = fieldId ? document.getElementById(fieldId) : null;
if (node) {
scrollIntoView(node, {
scrollMode: 'if-needed',
block: 'nearest',
...options,
});
}
},
getFieldInstance: (name: string) => {
const namePathStr = toNamePathStr(name);
return itemsRef.current[namePathStr];
},
},
[form, rcForm],
);
return [wrapForm];
}

View File

@ -0,0 +1,48 @@
import * as React from 'react';
import { useRef } from 'react';
import raf from 'raf';
type Updater<ValueType> = (prev?: ValueType) => ValueType;
export default function useFrameState<ValueType>(
defaultValue: ValueType,
): [ValueType, (updater: Updater<ValueType>) => void] {
const [value, setValue] = React.useState(defaultValue);
const frameRef = useRef<number | null>(null);
const batchRef = useRef<Updater<ValueType>[]>([]);
const destroyRef = useRef(false);
React.useEffect(
() => () => {
destroyRef.current = true;
raf.cancel(frameRef.current!);
},
[],
);
function setFrameValue(updater: Updater<ValueType>) {
if (destroyRef.current) {
return;
}
if (frameRef.current === null) {
batchRef.current = [];
frameRef.current = raf(() => {
frameRef.current = null;
setValue(prevValue => {
let current = prevValue;
batchRef.current.forEach(func => {
current = func(current);
});
return current;
});
});
}
batchRef.current.push(updater);
}
return [value, setFrameValue];
}

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import { composeRef } from 'rc-util/lib/ref';
import { FormContext } from '../context';
import { InternalNamePath } from '../interface';
export default function useItemRef() {
const { itemRef } = React.useContext(FormContext);
const cacheRef = React.useRef<{
name?: string;
originRef?: React.Ref<any>;
ref?: React.Ref<any>;
}>({});
function getRef(name: InternalNamePath, children: any) {
const childrenRef: React.Ref<React.ReactElement> =
children && typeof children === 'object' && children.ref;
const nameStr = name.join('_');
if (cacheRef.current.name !== nameStr || cacheRef.current.originRef !== childrenRef) {
cacheRef.current.name = nameStr;
cacheRef.current.originRef = childrenRef;
cacheRef.current.ref = composeRef(itemRef(name), childrenRef);
}
return cacheRef.current.ref;
}
return getRef;
}

View File

@ -185,21 +185,22 @@ Provide linkage between forms. If a sub form with `name` prop update, it will au
### FormInstance ### FormInstance
| Name | Description | Type | | Name | Description | Type | Version |
| --- | --- | --- | | --- | --- | --- | --- |
| getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any | | getFieldInstance | Get field instance | (name: [NamePath](#NamePath)) => any | 4.4.0 |
| getFieldsValue | Get values by a set of field names. Return according to the corresponding structure | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | | getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any | |
| getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] | | getFieldsValue | Get values by a set of field names. Return according to the corresponding structure | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | |
| getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | | getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] | |
| isFieldTouched | Check if a field has been operated | (name: [NamePath](#NamePath)) => boolean | | getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | |
| isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | | isFieldTouched | Check if a field has been operated | (name: [NamePath](#NamePath)) => boolean | |
| isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean | | isFieldsTouched | Check if fields have been operated. Check if all fields is touched when `allTouched` is `true` | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | |
| resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | | isFieldValidating | Check fields if is in validating | (name: [NamePath](#NamePath)) => boolean | |
| scrollToField | Scroll to field position | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | | resetFields | Reset fields to `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | |
| setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void | | scrollToField | Scroll to field position | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | |
| setFieldsValue | Set fields value | (values) => void | | setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void | |
| submit | Submit the form. It's same as click `submit` button | () => void | | setFieldsValue | Set fields value | (values) => void | |
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise | | submit | Submit the form. It's same as click `submit` button | () => void | |
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise | |
#### validateFields return sample #### validateFields return sample

View File

@ -186,21 +186,22 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
### FormInstance ### FormInstance
| 名称 | 说明 | 类型 | | 名称 | 说明 | 类型 | 版本 |
| --- | --- | --- | | --- | --- | --- | --- |
| getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any | | getFieldInstance | 获取对应字段示例 | (name: [NamePath](#NamePath)) => any | 4.4.0 |
| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | | getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any | |
| getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] | | getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | |
| getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | | getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] | |
| isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean | | getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | |
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | | isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean | |
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean | | isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | |
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | | isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean | |
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | | resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | |
| setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void | | scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | |
| setFieldsValue | 设置表单的值 | (values) => void | | setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void | |
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void | | setFieldsValue | 设置表单的值 | (values) => void | |
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise | | submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void | |
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise | |
#### validateFields 返回示例 #### validateFields 返回示例

View File

@ -1,3 +1,3 @@
export { Options as ScrollOptions } from 'scroll-into-view-if-needed'; export { Options as ScrollOptions } from 'scroll-into-view-if-needed';
export type FormLabelAlign = 'left' | 'right'; export type FormLabelAlign = 'left' | 'right';
export { Store, StoreValue } from 'rc-field-form/lib/interface'; export { Store, StoreValue, NamePath, InternalNamePath } from 'rc-field-form/lib/interface';

View File

@ -1,57 +1,4 @@
import * as React from 'react'; import { InternalNamePath } from './interface';
import raf from 'raf';
import { useForm as useRcForm, FormInstance as RcFormInstance } from 'rc-field-form';
import scrollIntoView from 'scroll-into-view-if-needed';
import { ScrollOptions } from './interface';
type InternalNamePath = (string | number)[];
/**
* Always debounce error to avoid [error -> null -> error] blink
*/
export function useCacheErrors(
errors: React.ReactNode[],
changeTrigger: (visible: boolean) => void,
directly: boolean,
): [boolean, React.ReactNode[]] {
const cacheRef = React.useRef({
errors,
visible: !!errors.length,
});
const [, forceUpdate] = React.useState({});
const update = () => {
const prevVisible = cacheRef.current.visible;
const newVisible = !!errors.length;
const prevErrors = cacheRef.current.errors;
cacheRef.current.errors = errors;
cacheRef.current.visible = newVisible;
if (prevVisible !== newVisible) {
changeTrigger(newVisible);
} else if (
prevErrors.length !== errors.length ||
prevErrors.some((prevErr, index) => prevErr !== errors[index])
) {
forceUpdate({});
}
};
React.useEffect(() => {
if (!directly) {
const timeout = setTimeout(update, 10);
return () => clearTimeout(timeout);
}
}, [errors]);
if (directly) {
update();
}
return [cacheRef.current.visible, cacheRef.current.errors];
}
export function toArray<T>(candidate?: T | T[] | false): T[] { export function toArray<T>(candidate?: T | T[] | false): T[] {
if (candidate === undefined || candidate === false) return []; if (candidate === undefined || candidate === false) return [];
@ -65,83 +12,3 @@ export function getFieldId(namePath: InternalNamePath, formName?: string): strin
const mergedId = namePath.join('_'); const mergedId = namePath.join('_');
return formName ? `${formName}_${mergedId}` : mergedId; return formName ? `${formName}_${mergedId}` : mergedId;
} }
export interface FormInstance extends RcFormInstance {
scrollToField: (name: string | number | InternalNamePath, options?: ScrollOptions) => void;
__INTERNAL__: {
name?: string;
};
}
export function useForm(form?: FormInstance): [FormInstance] {
const [rcForm] = useRcForm();
const wrapForm: FormInstance = React.useMemo(
() =>
form || {
...rcForm,
__INTERNAL__: {},
scrollToField: (name: string, options: ScrollOptions = {}) => {
const namePath = toArray(name);
const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
const node: HTMLElement | null = fieldId ? document.getElementById(fieldId) : null;
if (node) {
scrollIntoView(node, {
scrollMode: 'if-needed',
block: 'nearest',
...options,
});
}
},
},
[form, rcForm],
);
return [wrapForm];
}
type Updater<ValueType> = (prev?: ValueType) => ValueType;
export function useFrameState<ValueType>(
defaultValue: ValueType,
): [ValueType, (updater: Updater<ValueType>) => void] {
const [value, setValue] = React.useState(defaultValue);
const frameRef = React.useRef<number | null>(null);
const batchRef = React.useRef<Updater<ValueType>[]>([]);
const destroyRef = React.useRef(false);
React.useEffect(
() => () => {
destroyRef.current = true;
raf.cancel(frameRef.current!);
},
[],
);
function setFrameValue(updater: Updater<ValueType>) {
if (destroyRef.current) {
return;
}
if (frameRef.current === null) {
batchRef.current = [];
frameRef.current = raf(() => {
frameRef.current = null;
setValue(prevValue => {
let current = prevValue;
batchRef.current.forEach(func => {
current = func(current);
});
return current;
});
});
}
batchRef.current.push(updater);
}
return [value, setFrameValue];
}