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 omit from 'omit.js';
|
||||
import classNames from 'classnames';
|
||||
import FieldForm, { List } from 'rc-field-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 { FormContext } from './context';
|
||||
import { FormLabelAlign } from './interface';
|
||||
import { useForm, FormInstance } from './util';
|
||||
import useForm, { FormInstance } from './hooks/useForm';
|
||||
import SizeContext, { SizeType, SizeContextProvider } from '../config-provider/SizeContext';
|
||||
|
||||
export type FormLayout = 'horizontal' | 'inline' | 'vertical';
|
||||
@ -31,21 +30,24 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
const contextSize = React.useContext(SizeContext);
|
||||
const { getPrefixCls, direction }: ConfigConsumerProps = React.useContext(ConfigContext);
|
||||
|
||||
const { name } = props;
|
||||
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className = '',
|
||||
size = contextSize,
|
||||
form,
|
||||
colon,
|
||||
name,
|
||||
labelAlign,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
prefixCls: customizePrefixCls,
|
||||
hideRequiredMark,
|
||||
className = '',
|
||||
layout = 'horizontal',
|
||||
size = contextSize,
|
||||
scrollToFirstError,
|
||||
onFinishFailed,
|
||||
...restFormProps
|
||||
} = props;
|
||||
|
||||
const prefixCls = getPrefixCls('form', customizePrefixCls);
|
||||
|
||||
const formClassName = classNames(
|
||||
@ -59,20 +61,9 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
className,
|
||||
);
|
||||
|
||||
const formProps = omit(props, [
|
||||
'prefixCls',
|
||||
'className',
|
||||
'layout',
|
||||
'hideRequiredMark',
|
||||
'wrapperCol',
|
||||
'labelAlign',
|
||||
'labelCol',
|
||||
'colon',
|
||||
'scrollToFirstError',
|
||||
]);
|
||||
|
||||
const [wrapForm] = useForm(form);
|
||||
wrapForm.__INTERNAL__.name = name;
|
||||
const { __INTERNAL__ } = wrapForm;
|
||||
__INTERNAL__.name = name;
|
||||
|
||||
const formContextValue = React.useMemo(
|
||||
() => ({
|
||||
@ -82,6 +73,7 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
wrapperCol,
|
||||
vertical: layout === 'vertical',
|
||||
colon,
|
||||
itemRef: __INTERNAL__.itemRef,
|
||||
}),
|
||||
[name, labelAlign, labelCol, wrapperCol, layout, colon],
|
||||
);
|
||||
@ -100,12 +92,10 @@ const InternalForm: React.ForwardRefRenderFunction<unknown, FormProps> = (props,
|
||||
|
||||
return (
|
||||
<SizeContextProvider size={size}>
|
||||
<FormContext.Provider
|
||||
value={formContextValue}
|
||||
>
|
||||
<FormContext.Provider value={formContextValue}>
|
||||
<FieldForm
|
||||
id={name}
|
||||
{...formProps}
|
||||
{...restFormProps}
|
||||
onFinishFailed={onInternalFinishFailed}
|
||||
form={wrapForm}
|
||||
className={formClassName}
|
||||
|
@ -4,6 +4,7 @@ import classNames from 'classnames';
|
||||
import { Field, FormInstance } from 'rc-field-form';
|
||||
import { FieldProps } from 'rc-field-form/lib/Field';
|
||||
import { Meta, NamePath } from 'rc-field-form/lib/interface';
|
||||
import { supportRef } from 'rc-util/lib/ref';
|
||||
import omit from 'omit.js';
|
||||
import Row from '../grid/row';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
@ -12,8 +13,10 @@ import devWarning from '../_util/devWarning';
|
||||
import FormItemLabel, { FormItemLabelProps } from './FormItemLabel';
|
||||
import FormItemInput, { FormItemInputProps } from './FormItemInput';
|
||||
import { FormContext, FormItemContext } from './context';
|
||||
import { toArray, getFieldId, useFrameState } from './util';
|
||||
import { toArray, getFieldId } from './util';
|
||||
import { cloneElement, isValidElement } from '../_util/reactNode';
|
||||
import useFrameState from './hooks/useFrameState';
|
||||
import useItemRef from './hooks/useItemRef';
|
||||
|
||||
const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
|
||||
export type ValidateStatus = typeof ValidateStatuses[number];
|
||||
@ -80,7 +83,7 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
} = props;
|
||||
const destroyRef = React.useRef(false);
|
||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||
const formContext = React.useContext(FormContext);
|
||||
const { name: formName } = React.useContext(FormContext);
|
||||
const { updateItemErrors } = React.useContext(FormItemContext);
|
||||
const [domErrorVisible, innerSetDomErrorVisible] = React.useState(!!help);
|
||||
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);
|
||||
|
||||
// Cache Field NamePath
|
||||
@ -121,6 +123,9 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
}
|
||||
};
|
||||
|
||||
// ===================== Children Ref =====================
|
||||
const getItemRef = useItemRef();
|
||||
|
||||
function renderLayout(
|
||||
baseChildren: React.ReactNode,
|
||||
fieldId?: string,
|
||||
@ -316,6 +321,10 @@ function FormItem(props: FormItemProps): React.ReactElement {
|
||||
|
||||
const childProps = { ...children.props, ...mergedControl };
|
||||
|
||||
if (supportRef(children)) {
|
||||
childProps.ref = getItemRef(mergedName, children);
|
||||
}
|
||||
|
||||
// We should keep user origin event handler
|
||||
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 { ValidateStatus } from './FormItem';
|
||||
import { FormContext } from './context';
|
||||
import { useCacheErrors } from './util';
|
||||
import useCacheErrors from './hooks/useCacheErrors';
|
||||
|
||||
interface FormItemInputMiscProps {
|
||||
prefixCls: string;
|
||||
|
@ -2196,6 +2196,84 @@ exports[`renders ./components/form/demo/normal-login.md correctly 1`] = `
|
||||
</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`] = `
|
||||
<form
|
||||
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;
|
||||
labelCol?: ColProps;
|
||||
wrapperCol?: ColProps;
|
||||
itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
|
||||
}
|
||||
|
||||
export const FormContext = React.createContext<FormContextProps>({
|
||||
labelAlign: 'right',
|
||||
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
|
||||
|
||||
| Name | Description | Type |
|
||||
| --- | --- | --- |
|
||||
| getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any |
|
||||
| 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 |
|
||||
| getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] |
|
||||
| getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] |
|
||||
| isFieldTouched | Check if a field has been operated | (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 |
|
||||
| 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), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void |
|
||||
| setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void |
|
||||
| setFieldsValue | Set fields value | (values) => void |
|
||||
| submit | Submit the form. It's same as click `submit` button | () => void |
|
||||
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise |
|
||||
| Name | Description | Type | Version |
|
||||
| --- | --- | --- | --- |
|
||||
| getFieldInstance | Get field instance | (name: [NamePath](#NamePath)) => any | 4.4.0 |
|
||||
| getFieldValue | Get the value by the field name | (name: [NamePath](#NamePath)) => any | |
|
||||
| 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 | |
|
||||
| getFieldError | Get the error messages by the field name | (name: [NamePath](#NamePath)) => string[] | |
|
||||
| getFieldsError | Get the error messages by the fields name. Return as an array | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | |
|
||||
| isFieldTouched | Check if a field has been operated | (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 | |
|
||||
| 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), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | |
|
||||
| setFields | Set fields status | (fields: [FieldData](#FieldData)[]) => void | |
|
||||
| setFieldsValue | Set fields value | (values) => void | |
|
||||
| submit | Submit the form. It's same as click `submit` button | () => void | |
|
||||
| validateFields | Validate fields | (nameList?: [NamePath](#NamePath)[]) => Promise | |
|
||||
|
||||
#### validateFields return sample
|
||||
|
||||
|
@ -186,21 +186,22 @@ Form 通过增量更新方式,只更新被修改的字段相关组件以达到
|
||||
|
||||
### FormInstance
|
||||
|
||||
| 名称 | 说明 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any |
|
||||
| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any |
|
||||
| getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] |
|
||||
| getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] |
|
||||
| isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean |
|
||||
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean |
|
||||
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean |
|
||||
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void |
|
||||
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void |
|
||||
| setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void |
|
||||
| setFieldsValue | 设置表单的值 | (values) => void |
|
||||
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void |
|
||||
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise |
|
||||
| 名称 | 说明 | 类型 | 版本 |
|
||||
| --- | --- | --- | --- |
|
||||
| getFieldInstance | 获取对应字段示例 | (name: [NamePath](#NamePath)) => any | 4.4.0 |
|
||||
| getFieldValue | 获取对应字段名的值 | (name: [NamePath](#NamePath)) => any | |
|
||||
| getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回 | (nameList?: [NamePath](#NamePath)[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any | |
|
||||
| getFieldError | 获取对应字段名的错误信息 | (name: [NamePath](#NamePath)) => string[] | |
|
||||
| getFieldsError | 获取一组字段名对应的错误信息,返回为数组形式 | (nameList?: [NamePath](#NamePath)[]) => FieldError[] | |
|
||||
| isFieldTouched | 检查对应字段是否被用户操作过 | (name: [NamePath](#NamePath)) => boolean | |
|
||||
| isFieldsTouched | 检查一组字段是否被用户操作过,`allTouched` 为 `true` 时检查是否所有字段都被操作过 | (nameList?: [NamePath](#NamePath)[], allTouched?: boolean) => boolean | |
|
||||
| isFieldValidating | 检查一组字段是否正在校验 | (name: [NamePath](#NamePath)) => boolean | |
|
||||
| resetFields | 重置一组字段到 `initialValues` | (fields?: [NamePath](#NamePath)[]) => void | |
|
||||
| scrollToField | 滚动到对应字段位置 | (name: [NamePath](#NamePath), options: [[ScrollOptions](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)]) => void | |
|
||||
| setFields | 设置一组字段状态 | (fields: [FieldData](#FieldData)[]) => void | |
|
||||
| setFieldsValue | 设置表单的值 | (values) => void | |
|
||||
| submit | 提交表单,与点击 `submit` 按钮效果相同 | () => void | |
|
||||
| validateFields | 触发表单验证 | (nameList?: [NamePath](#NamePath)[]) => Promise | |
|
||||
|
||||
#### validateFields 返回示例
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
export { Options as ScrollOptions } from 'scroll-into-view-if-needed';
|
||||
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 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];
|
||||
}
|
||||
import { InternalNamePath } from './interface';
|
||||
|
||||
export function toArray<T>(candidate?: T | T[] | false): T[] {
|
||||
if (candidate === undefined || candidate === false) return [];
|
||||
@ -65,83 +12,3 @@ export function getFieldId(namePath: InternalNamePath, formName?: string): strin
|
||||
const mergedId = namePath.join('_');
|
||||
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