Skip to content

Commit

Permalink
feat: form instance support getFieldInstance (#24711)
Browse files Browse the repository at this point in the history
* support getFieldInstance

* update doc

* fix lint

* move func

* move into hooks

* update ref logic

* fix lint

* rm only

* fix docs
  • Loading branch information
zombieJ authored Jun 5, 2020
1 parent c2beed8 commit e46d414
Show file tree
Hide file tree
Showing 15 changed files with 480 additions and 192 deletions.
36 changes: 13 additions & 23 deletions components/form/Form.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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(
Expand All @@ -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(
() => ({
Expand All @@ -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],
);
Expand All @@ -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}
Expand Down
15 changes: 12 additions & 3 deletions components/form/FormItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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];
Expand Down Expand Up @@ -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);
Expand All @@ -92,7 +95,6 @@ function FormItem(props: FormItemProps): React.ReactElement {
}
}

const { name: formName } = formContext;
const hasName = hasValidName(name);

// Cache Field NamePath
Expand Down Expand Up @@ -121,6 +123,9 @@ function FormItem(props: FormItemProps): React.ReactElement {
}
};

// ===================== Children Ref =====================
const getItemRef = useItemRef();

function renderLayout(
baseChildren: React.ReactNode,
fieldId?: string,
Expand Down Expand Up @@ -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)]);

Expand Down
2 changes: 1 addition & 1 deletion components/form/FormItemInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
78 changes: 78 additions & 0 deletions components/form/__tests__/__snapshots__/demo.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
91 changes: 91 additions & 0 deletions components/form/__tests__/ref.test.tsx
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 2 additions & 0 deletions components/form/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

/**
Expand Down
Loading

0 comments on commit e46d414

Please sign in to comment.