From e46d414b110f3ebf74c91212afd96cf0a76b7305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 5 Jun 2020 18:06:52 +0800 Subject: [PATCH] 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 --- components/form/Form.tsx | 36 ++--- components/form/FormItem.tsx | 15 +- components/form/FormItemInput.tsx | 2 +- .../__tests__/__snapshots__/demo.test.js.snap | 78 ++++++++++ components/form/__tests__/ref.test.tsx | 91 ++++++++++++ components/form/context.tsx | 2 + components/form/demo/ref-item.md | 61 ++++++++ components/form/hooks/useCacheErrors.ts | 48 +++++++ components/form/hooks/useForm.ts | 64 +++++++++ components/form/hooks/useFrameState.ts | 48 +++++++ components/form/hooks/useItemRef.ts | 28 ++++ components/form/index.en-US.md | 31 ++-- components/form/index.zh-CN.md | 31 ++-- components/form/interface.ts | 2 +- components/form/util.ts | 135 +----------------- 15 files changed, 480 insertions(+), 192 deletions(-) create mode 100644 components/form/__tests__/ref.test.tsx create mode 100644 components/form/demo/ref-item.md create mode 100644 components/form/hooks/useCacheErrors.ts create mode 100644 components/form/hooks/useForm.ts create mode 100644 components/form/hooks/useFrameState.ts create mode 100644 components/form/hooks/useItemRef.ts diff --git a/components/form/Form.tsx b/components/form/Form.tsx index 75f788750e..2f884d403c 100644 --- a/components/form/Form.tsx +++ b/components/form/Form.tsx @@ -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 = (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 = (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 = (props, wrapperCol, vertical: layout === 'vertical', colon, + itemRef: __INTERNAL__.itemRef, }), [name, labelAlign, labelCol, wrapperCol, layout, colon], ); @@ -100,12 +92,10 @@ const InternalForm: React.ForwardRefRenderFunction = (props, return ( - + (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([...toArray(trigger), ...toArray(validateTrigger)]); diff --git a/components/form/FormItemInput.tsx b/components/form/FormItemInput.tsx index 4690b3e549..039c445f69 100644 --- a/components/form/FormItemInput.tsx +++ b/components/form/FormItemInput.tsx @@ -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; diff --git a/components/form/__tests__/__snapshots__/demo.test.js.snap b/components/form/__tests__/__snapshots__/demo.test.js.snap index d20cd34667..1fbe9ec984 100644 --- a/components/form/__tests__/__snapshots__/demo.test.js.snap +++ b/components/form/__tests__/__snapshots__/demo.test.js.snap @@ -2196,6 +2196,84 @@ exports[`renders ./components/form/demo/normal-login.md correctly 1`] = ` `; +exports[`renders ./components/form/demo/ref-item.md correctly 1`] = ` +
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+ + +
+`; + exports[`renders ./components/form/demo/register.md correctly 1`] = `
{ + const Test = ({ + onRef, + show, + }: { + onRef: (node: React.ReactElement, originRef: React.RefObject) => void; + show?: boolean; + }) => { + const [form] = Form.useForm(); + const removeRef = React.useRef(); + const testRef = React.useRef(); + const listRef = React.useRef(); + + return ( + + {show && ( + + + + )} + + + + + + + {fields => + fields.map(field => ( + + + + )) + } + + + + + + + ); + }; + + it('should ref work', () => { + const onRef = jest.fn(); + const wrapper = mount(); + + 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); + }); +}); diff --git a/components/form/context.tsx b/components/form/context.tsx index 73dfb8c3a2..f87fc786c1 100644 --- a/components/form/context.tsx +++ b/components/form/context.tsx @@ -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({ labelAlign: 'right', vertical: false, + itemRef: (() => {}) as any, }); /** diff --git a/components/form/demo/ref-item.md b/components/form/demo/ref-item.md new file mode 100644 index 0000000000..dadd38ce13 --- /dev/null +++ b/components/form/demo/ref-item.md @@ -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 ( +
+ + + + + + {fields => + fields.map(field => ( + + + + )) + } + + + + +
+ ); +}; + +ReactDOM.render(, mountNode); +``` diff --git a/components/form/hooks/useCacheErrors.ts b/components/form/hooks/useCacheErrors.ts new file mode 100644 index 0000000000..149841f5e6 --- /dev/null +++ b/components/form/hooks/useCacheErrors.ts @@ -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]; +} diff --git a/components/form/hooks/useForm.ts b/components/form/hooks/useForm.ts new file mode 100644 index 0000000000..521d16eacd --- /dev/null +++ b/components/form/hooks/useForm.ts @@ -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>({}); + + 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]; +} diff --git a/components/form/hooks/useFrameState.ts b/components/form/hooks/useFrameState.ts new file mode 100644 index 0000000000..10d2bd4af1 --- /dev/null +++ b/components/form/hooks/useFrameState.ts @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { useRef } from 'react'; +import raf from 'raf'; + +type Updater = (prev?: ValueType) => ValueType; + +export default function useFrameState( + defaultValue: ValueType, +): [ValueType, (updater: Updater) => void] { + const [value, setValue] = React.useState(defaultValue); + const frameRef = useRef(null); + const batchRef = useRef[]>([]); + const destroyRef = useRef(false); + + React.useEffect( + () => () => { + destroyRef.current = true; + raf.cancel(frameRef.current!); + }, + [], + ); + + function setFrameValue(updater: Updater) { + 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]; +} diff --git a/components/form/hooks/useItemRef.ts b/components/form/hooks/useItemRef.ts new file mode 100644 index 0000000000..1e96ef8cb5 --- /dev/null +++ b/components/form/hooks/useItemRef.ts @@ -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; + ref?: React.Ref; + }>({}); + + function getRef(name: InternalNamePath, children: any) { + const childrenRef: React.Ref = + 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; +} diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md index 5b0adc9274..52edacadd8 100644 --- a/components/form/index.en-US.md +++ b/components/form/index.en-US.md @@ -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 diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md index 8b46e11e7f..10733fef37 100644 --- a/components/form/index.zh-CN.md +++ b/components/form/index.zh-CN.md @@ -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 返回示例 diff --git a/components/form/interface.ts b/components/form/interface.ts index f9ec7a0e76..eae8bf49b5 100644 --- a/components/form/interface.ts +++ b/components/form/interface.ts @@ -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'; diff --git a/components/form/util.ts b/components/form/util.ts index 2ab1242cb6..d32067fb33 100644 --- a/components/form/util.ts +++ b/components/form/util.ts @@ -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(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 = (prev?: ValueType) => ValueType; - -export function useFrameState( - defaultValue: ValueType, -): [ValueType, (updater: Updater) => void] { - const [value, setValue] = React.useState(defaultValue); - const frameRef = React.useRef(null); - const batchRef = React.useRef[]>([]); - const destroyRef = React.useRef(false); - - React.useEffect( - () => () => { - destroyRef.current = true; - raf.cancel(frameRef.current!); - }, - [], - ); - - function setFrameValue(updater: Updater) { - 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]; -}