mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-24 19:19:57 +08:00
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:
parent
c2beed8bd3
commit
e46d414b11
@ -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}
|
||||||
|
@ -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)]);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
||||||
|
91
components/form/__tests__/ref.test.tsx
Normal file
91
components/form/__tests__/ref.test.tsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
61
components/form/demo/ref-item.md
Normal file
61
components/form/demo/ref-item.md
Normal 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);
|
||||||
|
```
|
48
components/form/hooks/useCacheErrors.ts
Normal file
48
components/form/hooks/useCacheErrors.ts
Normal 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];
|
||||||
|
}
|
64
components/form/hooks/useForm.ts
Normal file
64
components/form/hooks/useForm.ts
Normal 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];
|
||||||
|
}
|
48
components/form/hooks/useFrameState.ts
Normal file
48
components/form/hooks/useFrameState.ts
Normal 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];
|
||||||
|
}
|
28
components/form/hooks/useItemRef.ts
Normal file
28
components/form/hooks/useItemRef.ts
Normal 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;
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
@ -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 返回示例
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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];
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user