mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-12-01 03:29:39 +08:00
commit
f8ed0480de
@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { forwardRef, useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import CalendarOutlined from '@ant-design/icons/CalendarOutlined';
|
||||
import ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined';
|
||||
@ -7,14 +8,14 @@ import SwapRightOutlined from '@ant-design/icons/SwapRightOutlined';
|
||||
import { RangePicker as RCRangePicker } from 'rc-picker';
|
||||
import { GenerateConfig } from 'rc-picker/lib/generate/index';
|
||||
import enUS from '../locale/en_US';
|
||||
import { ConfigContext, ConfigConsumerProps } from '../../config-provider';
|
||||
import { ConfigConsumerProps, ConfigContext } from '../../config-provider';
|
||||
import SizeContext from '../../config-provider/SizeContext';
|
||||
import LocaleReceiver from '../../locale-provider/LocaleReceiver';
|
||||
import { getRangePlaceholder, transPlacement2DropdownAlign } from '../util';
|
||||
import { RangePickerProps, PickerLocale, getTimeProps, Components } from '.';
|
||||
import { PickerComponentClass } from './interface';
|
||||
import { Components, getTimeProps, PickerLocale, RangePickerProps } from '.';
|
||||
import { FormItemInputContext } from '../../form/context';
|
||||
import { getMergedStatus, getStatusClassNames } from '../../_util/statusUtils';
|
||||
import { PickerComponentClass } from './interface';
|
||||
|
||||
export default function generateRangePicker<DateType>(
|
||||
generateConfig: GenerateConfig<DateType>,
|
||||
@ -42,7 +43,7 @@ export default function generateRangePicker<DateType>(
|
||||
const locale = { ...contextLocale, ...this.props.locale };
|
||||
const { getPrefixCls, direction, getPopupContainer } = this.context;
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
prefixCls,
|
||||
getPopupContainer: customGetPopupContainer,
|
||||
className,
|
||||
placement,
|
||||
@ -53,7 +54,6 @@ export default function generateRangePicker<DateType>(
|
||||
...restProps
|
||||
} = this.props;
|
||||
const { format, showTime, picker } = this.props as any;
|
||||
const prefixCls = getPrefixCls('picker', customizePrefixCls);
|
||||
|
||||
let additionalOverrideProps: any = {};
|
||||
|
||||
@ -105,7 +105,7 @@ export default function generateRangePicker<DateType>(
|
||||
[`${prefixCls}-borderless`]: !bordered,
|
||||
},
|
||||
getStatusClassNames(
|
||||
prefixCls,
|
||||
prefixCls as string,
|
||||
getMergedStatus(contextStatus, customStatus),
|
||||
hasFeedback,
|
||||
),
|
||||
@ -136,5 +136,12 @@ export default function generateRangePicker<DateType>(
|
||||
}
|
||||
}
|
||||
|
||||
return RangePicker;
|
||||
return forwardRef<RangePicker, RangePickerProps<DateType>>((props, ref) => {
|
||||
const { prefixCls: customizePrefixCls } = props;
|
||||
|
||||
const { getPrefixCls } = useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('picker', customizePrefixCls);
|
||||
|
||||
return <RangePicker {...props} prefixCls={prefixCls} ref={ref} />;
|
||||
}) as unknown as PickerComponentClass<RangePickerProps<DateType>>;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import CloseCircleFilled from '@ant-design/icons/CloseCircleFilled';
|
||||
import RCPicker from 'rc-picker';
|
||||
import { PickerMode } from 'rc-picker/lib/interface';
|
||||
import { GenerateConfig } from 'rc-picker/lib/generate/index';
|
||||
import { forwardRef, useContext } from 'react';
|
||||
import enUS from '../locale/en_US';
|
||||
import { getPlaceholder, transPlacement2DropdownAlign } from '../util';
|
||||
import devWarning from '../../_util/devWarning';
|
||||
@ -20,9 +21,9 @@ import {
|
||||
getTimeProps,
|
||||
Components,
|
||||
} from '.';
|
||||
import { PickerComponentClass } from './interface';
|
||||
import { FormItemInputContext } from '../../form/context';
|
||||
import { getMergedStatus, getStatusClassNames, InputStatus } from '../../_util/statusUtils';
|
||||
import { DatePickRef, PickerComponentClass } from './interface';
|
||||
|
||||
export default function generatePicker<DateType>(generateConfig: GenerateConfig<DateType>) {
|
||||
type DatePickerProps = PickerProps<DateType> & {
|
||||
@ -67,7 +68,7 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
|
||||
const locale = { ...contextLocale, ...this.props.locale };
|
||||
const { getPrefixCls, direction, getPopupContainer } = this.context;
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
prefixCls,
|
||||
getPopupContainer: customizeGetPopupContainer,
|
||||
className,
|
||||
size: customizeSize,
|
||||
@ -78,7 +79,6 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
|
||||
...restProps
|
||||
} = this.props;
|
||||
const { format, showTime } = this.props as any;
|
||||
const prefixCls = getPrefixCls('picker', customizePrefixCls);
|
||||
|
||||
const additionalProps = {
|
||||
showToday: true,
|
||||
@ -137,7 +137,7 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
|
||||
[`${prefixCls}-borderless`]: !bordered,
|
||||
},
|
||||
getStatusClassNames(
|
||||
prefixCls,
|
||||
prefixCls as string,
|
||||
getMergedStatus(contextStatus, customStatus),
|
||||
hasFeedback,
|
||||
),
|
||||
@ -167,11 +167,26 @@ export default function generatePicker<DateType>(generateConfig: GenerateConfig<
|
||||
}
|
||||
}
|
||||
|
||||
const PickerWrapper = forwardRef<DatePickRef<DateType>, InnerPickerProps>((props, ref) => {
|
||||
const { prefixCls: customizePrefixCls } = props;
|
||||
|
||||
const { getPrefixCls } = useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('picker', customizePrefixCls);
|
||||
|
||||
const pickerProps: InnerPickerProps = {
|
||||
...props,
|
||||
prefixCls,
|
||||
ref,
|
||||
};
|
||||
|
||||
return <Picker {...pickerProps} />;
|
||||
});
|
||||
|
||||
if (displayName) {
|
||||
Picker.displayName = displayName;
|
||||
PickerWrapper.displayName = displayName;
|
||||
}
|
||||
|
||||
return Picker as PickerComponentClass<InnerPickerProps>;
|
||||
return PickerWrapper as unknown as PickerComponentClass<InnerPickerProps>;
|
||||
}
|
||||
|
||||
const DatePicker = getPicker<DatePickerProps>();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import FieldForm, { List } from 'rc-field-form';
|
||||
import FieldForm, { List, useWatch } from 'rc-field-form';
|
||||
import { FormProps as RcFormProps } from 'rc-field-form/lib/Form';
|
||||
import { ValidateErrorEntity } from 'rc-field-form/lib/interface';
|
||||
import { Options } from 'scroll-into-view-if-needed';
|
||||
@ -101,8 +101,9 @@ const InternalForm: React.ForwardRefRenderFunction<FormInstance, FormProps> = (p
|
||||
colon: mergedColon,
|
||||
requiredMark: mergedRequiredMark,
|
||||
itemRef: __INTERNAL__.itemRef,
|
||||
form: wrapForm,
|
||||
}),
|
||||
[name, labelAlign, labelCol, wrapperCol, layout, mergedColon, mergedRequiredMark],
|
||||
[name, labelAlign, labelCol, wrapperCol, layout, mergedColon, mergedRequiredMark, wrapForm],
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => wrapForm);
|
||||
@ -140,6 +141,6 @@ const Form = React.forwardRef<FormInstance, FormProps>(InternalForm) as <Values
|
||||
props: React.PropsWithChildren<FormProps<Values>> & { ref?: React.Ref<FormInstance<Values>> },
|
||||
) => React.ReactElement;
|
||||
|
||||
export { useForm, List, FormInstance };
|
||||
export { useForm, List, FormInstance, useWatch };
|
||||
|
||||
export default Form;
|
||||
|
@ -14850,6 +14850,157 @@ exports[`renders ./components/form/demo/time-related-controls.md extend context
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/form/demo/useWatch.md extend context correctly 1`] = `
|
||||
Array [
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ant-form ant-form-vertical"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="name"
|
||||
title="Name (Watch to trigger rerender)"
|
||||
>
|
||||
Name (Watch to trigger rerender)
|
||||
</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="name"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="age"
|
||||
title="Age (Not Watch)"
|
||||
>
|
||||
Age (Not Watch)
|
||||
</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"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number ant-input-number-in-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
id="age"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>,
|
||||
<article
|
||||
class="ant-typography"
|
||||
>
|
||||
<pre>
|
||||
Name Value:
|
||||
</pre>
|
||||
</article>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/form/demo/validate-other.md extend context correctly 1`] = `
|
||||
<form
|
||||
class="ant-form ant-form-horizontal"
|
||||
|
@ -6098,6 +6098,157 @@ exports[`renders ./components/form/demo/time-related-controls.md correctly 1`] =
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/form/demo/useWatch.md correctly 1`] = `
|
||||
Array [
|
||||
<form
|
||||
autocomplete="off"
|
||||
class="ant-form ant-form-vertical"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="name"
|
||||
title="Name (Watch to trigger rerender)"
|
||||
>
|
||||
Name (Watch to trigger rerender)
|
||||
</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="name"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="age"
|
||||
title="Age (Not Watch)"
|
||||
>
|
||||
Age (Not Watch)
|
||||
</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"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number ant-input-number-in-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
id="age"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>,
|
||||
<article
|
||||
class="ant-typography"
|
||||
>
|
||||
<pre>
|
||||
Name Value:
|
||||
</pre>
|
||||
</article>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
|
||||
<form
|
||||
class="ant-form ant-form-horizontal"
|
||||
|
@ -1079,4 +1079,30 @@ describe('Form', () => {
|
||||
expect(wrapper.find('.ant-form-item-no-colon')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('useFormInstance', () => {
|
||||
let formInstance;
|
||||
let subFormInstance;
|
||||
|
||||
const Sub = () => {
|
||||
const formSub = Form.useFormInstance();
|
||||
subFormInstance = formSub;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const Demo = () => {
|
||||
const [form] = Form.useForm();
|
||||
formInstance = form;
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<Sub />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Demo />);
|
||||
expect(subFormInstance).toBe(formInstance);
|
||||
});
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import { FormProviderProps as RcFormProviderProps } from 'rc-field-form/lib/Form
|
||||
import { FC, PropsWithChildren, ReactNode, useMemo } from 'react';
|
||||
import { ColProps } from '../grid/col';
|
||||
import { FormLabelAlign } from './interface';
|
||||
import { RequiredMark } from './Form';
|
||||
import { FormInstance, RequiredMark } from './Form';
|
||||
import { ValidateStatus } from './FormItem';
|
||||
|
||||
/** Form Context. Set top form style and pass to Form Item usage. */
|
||||
@ -20,6 +20,7 @@ export interface FormContextProps {
|
||||
wrapperCol?: ColProps;
|
||||
requiredMark?: RequiredMark;
|
||||
itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void;
|
||||
form?: FormInstance;
|
||||
}
|
||||
|
||||
export const FormContext = React.createContext<FormContextProps>({
|
||||
|
44
components/form/demo/useWatch.md
Normal file
44
components/form/demo/useWatch.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
order: 3.3
|
||||
version: 4.20.0
|
||||
title:
|
||||
zh-CN: 字段监听 Hooks
|
||||
en-US: Watch Hooks
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
`useWatch` 允许你监听字段变化,同时仅当改字段变化时重新渲染。API 文档请[查阅此处](#Form.useWatch)。
|
||||
|
||||
## en-US
|
||||
|
||||
`useWatch` helps watch the field change and only re-render for the value change. [API Ref](#Form.useWatch).
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Form, Input, InputNumber, Typography } from 'antd';
|
||||
|
||||
const Demo = () => {
|
||||
const [form] = Form.useForm<{ user: { name: string; age: number } }>();
|
||||
const nameValue = Form.useWatch('name', form);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form form={form} layout="vertical" autoComplete="off">
|
||||
<Form.Item name="name" label="Name (Watch to trigger rerender)">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="age" label="Age (Not Watch)">
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Typography>
|
||||
<pre>Name Value: {nameValue}</pre>
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Demo;
|
||||
```
|
9
components/form/hooks/useFormInstance.ts
Normal file
9
components/form/hooks/useFormInstance.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
import { FormContext } from '../context';
|
||||
import { FormInstance } from './useForm';
|
||||
|
||||
export default function useFormInstance<Value = any>(): FormInstance<Value> {
|
||||
const { form } = useContext(FormContext);
|
||||
|
||||
return form!;
|
||||
}
|
@ -279,6 +279,65 @@ validateFields()
|
||||
});
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
### Form.useForm
|
||||
|
||||
`type Form.useForm = (): FormInstance`
|
||||
|
||||
Create Form instance to maintain data store.
|
||||
|
||||
### Form.useFormInstance
|
||||
|
||||
`type Form.useFormInstance = (): FormInstance`
|
||||
|
||||
Added in `4.20.0`. Get current context form instance to avoid pass as props between components:
|
||||
|
||||
```tsx
|
||||
const Sub = () => {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
return <Button onClick={() => form.setFieldsValue({})} />;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<Sub />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Form.useWatch
|
||||
|
||||
`type Form.useWatch = (namePath: NamePath, formInstance: FormInstance): Value`
|
||||
|
||||
Added in `4.20.0`. Watch the value of a field. You can use this to interactive with other hooks like `useSWR` to reduce develop cost:
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const [form] = Form.useForm();
|
||||
const userName = Form.useWatch('username', form);
|
||||
|
||||
const { data: options } = useSWR(`/api/user/${userName}`, fetcher);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<Form.Item name="username">
|
||||
<AutoComplete options={options} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Difference between other data fetching method
|
||||
|
||||
Form only update the Field which changed to avoid full refresh perf issue. Thus you can not get real time value with `getFieldsValue` in render. And `useWatch` will rerender current component to sync with latest value. You can also use Field renderProps to get better performance if only want to do conditional render. If component no need care field value change, you can use `onValuesChange` to give to parent component to avoid current one rerender.
|
||||
|
||||
### Interface
|
||||
|
||||
#### NamePath
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { Rule, RuleObject, RuleRender } from 'rc-field-form/lib/interface';
|
||||
import InternalForm, { useForm, FormInstance, FormProps } from './Form';
|
||||
import InternalForm, { useForm, FormInstance, FormProps, useWatch } from './Form';
|
||||
import Item, { FormItemProps } from './FormItem';
|
||||
import ErrorList, { ErrorListProps } from './ErrorList';
|
||||
import List, { FormListProps } from './FormList';
|
||||
import { FormProvider } from './context';
|
||||
import devWarning from '../_util/devWarning';
|
||||
import useFormInstance from './hooks/useFormInstance';
|
||||
|
||||
type InternalFormType = typeof InternalForm;
|
||||
|
||||
interface FormInterface extends InternalFormType {
|
||||
useForm: typeof useForm;
|
||||
useFormInstance: typeof useFormInstance;
|
||||
useWatch: typeof useWatch;
|
||||
Item: typeof Item;
|
||||
List: typeof List;
|
||||
ErrorList: typeof ErrorList;
|
||||
@ -25,6 +28,8 @@ Form.Item = Item;
|
||||
Form.List = List;
|
||||
Form.ErrorList = ErrorList;
|
||||
Form.useForm = useForm;
|
||||
Form.useFormInstance = useFormInstance;
|
||||
Form.useWatch = useWatch;
|
||||
Form.Provider = FormProvider;
|
||||
Form.create = () => {
|
||||
devWarning(
|
||||
|
@ -278,6 +278,65 @@ validateFields()
|
||||
});
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
### Form.useForm
|
||||
|
||||
`type Form.useForm = (): FormInstance`
|
||||
|
||||
创建 Form 实例,用于管理所有数据状态。
|
||||
|
||||
### Form.useFormInstance
|
||||
|
||||
`type Form.useFormInstance = (): FormInstance`
|
||||
|
||||
`4.20.0` 新增,获取当前上下文正在使用的 Form 实例,常见于封装子组件消费无需透传 Form 实例:
|
||||
|
||||
```tsx
|
||||
const Sub = () => {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
return <Button onClick={() => form.setFieldsValue({})} />;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<Sub />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Form.useWatch
|
||||
|
||||
`type Form.useWatch = (namePath: NamePath, formInstance: FormInstance): Value`
|
||||
|
||||
`4.20.0` 新增,用于直接获取 form 中字段对应的值。通过该 Hooks 可以与诸如 `useSWR` 进行联动从而降低维护成本:
|
||||
|
||||
```tsx
|
||||
const Demo = () => {
|
||||
const [form] = Form.useForm();
|
||||
const userName = Form.useWatch('username', form);
|
||||
|
||||
const { data: options } = useSWR(`/api/user/${userName}`, fetcher);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<Form.Item name="username">
|
||||
<AutoComplete options={options} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 与其他获取数据的方式的区别
|
||||
|
||||
Form 仅会对变更的 Field 进行刷新,从而避免完整的组件刷新可能引发的性能问题。因而你无法在 render 阶段通过 `form.getFieldsValue` 来实时获取字段值,而 `useWatch` 提供了一种特定字段访问的方式,从而使得在当前组件中可以直接消费字段的值。同时,如果为了更好的渲染性能,你可以通过 Field 的 renderProps 仅更新需要更新的部分。而当当前组件更新或者 effect 都不需要消费字段值时,则可以通过 `onValuesChange` 将数据抛出,从而避免组件更新。
|
||||
|
||||
### Interface
|
||||
|
||||
#### NamePath
|
||||
@ -439,7 +498,7 @@ React 中异步更新会导致受控组件交互行为异常。当用户交互
|
||||
|
||||
### `setFieldsValue` 不会触发 `onFieldsChange` 和 `onValuesChange`?
|
||||
|
||||
是的,change 事件仅当用户交互才会触发。该设计是为了防止在 change 事件中调用 `setFieldsValue` 导致的循环问题。
|
||||
是的,change 事件仅当用户交互才会触发。该设计是为了防止在 change 事件中调用 `setFieldsValue` 导致的循环问题。如果仅仅需要组件内消费,可以通过 `useWatch` 或者 `Field.renderProps` 来实现。
|
||||
|
||||
### 有更多参考文档吗?
|
||||
|
||||
|
@ -37,7 +37,8 @@ Previewable image.
|
||||
src?: string; // V4.10.0
|
||||
mask?: ReactNode; // V4.9.0
|
||||
maskClassName?: string; // V4.11.0
|
||||
current?: number; // V4.12.0 Only support PreviewGroup.
|
||||
current?: number; // V4.12.0 Only support PreviewGroup
|
||||
countRender?: (current: number, total: number) => string // Only support PreviewGroup
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -38,7 +38,8 @@ cover: https://gw.alipayobjects.com/zos/antfincdn/D1dXz9PZqa/image.svg
|
||||
src?: string; // V4.10.0
|
||||
mask?: ReactNode; // V4.9.0
|
||||
maskClassName?: string; // V4.11.0
|
||||
current?: number; // V4.12.0 仅支持 PreviewGroup。
|
||||
current?: number; // V4.12.0 仅支持 PreviewGroup
|
||||
countRender?: (current: number, total: number) => string // 仅支持 PreviewGroup
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -47,7 +47,7 @@ describe('TimePicker', () => {
|
||||
it('clearIcon should render correctly', () => {
|
||||
const clearIcon = <div className="test-clear-icon">test</div>;
|
||||
const wrapper = mount(<TimePicker clearIcon={clearIcon} />);
|
||||
expect(wrapper.find('Picker').prop('clearIcon')).toEqual(
|
||||
expect(wrapper.find('Picker').last().prop('clearIcon')).toEqual(
|
||||
<div className="test-clear-icon">test</div>,
|
||||
);
|
||||
});
|
||||
@ -70,7 +70,7 @@ describe('TimePicker', () => {
|
||||
popupClassName={popupClassName}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.find('Picker').prop('dropdownClassName')).toEqual(popupClassName);
|
||||
expect(wrapper.find('Picker').last().prop('dropdownClassName')).toEqual(popupClassName);
|
||||
});
|
||||
|
||||
it('should pass popupClassName prop to RangePicker as dropdownClassName prop', () => {
|
||||
|
@ -104,6 +104,7 @@ const ListItem = React.forwardRef(
|
||||
src={file.thumbUrl || file.url}
|
||||
alt={file.name}
|
||||
className={`${prefixCls}-list-item-image`}
|
||||
crossOrigin={file.crossOrigin}
|
||||
/>
|
||||
) : (
|
||||
iconNode
|
||||
|
@ -1241,4 +1241,134 @@ describe('Upload List', () => {
|
||||
const wrapper = mount(<Upload fileList={null} />);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should not exist crossorigin attribute when does not set file.crossorigin in case of listType="picture"', () => {
|
||||
const list = [
|
||||
{
|
||||
uid: '0',
|
||||
name: 'xxx.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = mount(
|
||||
<Upload fileList={list} listType="picture">
|
||||
<button type="button">upload</button>
|
||||
</Upload>,
|
||||
);
|
||||
list.forEach((file, i) => {
|
||||
const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img').at(i);
|
||||
expect(imgNode.prop('crossOrigin')).toBe(undefined);
|
||||
expect(imgNode.prop('crossOrigin')).toBe(file.crossOrigin);
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should exist crossorigin attribute when set file.crossorigin in case of listType="picture"', () => {
|
||||
const list = [
|
||||
{
|
||||
uid: '0',
|
||||
name: 'xxx.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
|
||||
crossOrigin: '',
|
||||
},
|
||||
{
|
||||
uid: '1',
|
||||
name: 'xxx.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
{
|
||||
uid: '2',
|
||||
name: 'xxx.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
|
||||
crossOrigin: 'use-credentials',
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = mount(
|
||||
<Upload fileList={list} listType="picture">
|
||||
<button type="button">upload</button>
|
||||
</Upload>,
|
||||
);
|
||||
list.forEach((file, i) => {
|
||||
const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img').at(i);
|
||||
expect(imgNode.prop('crossOrigin')).not.toBe(undefined);
|
||||
expect(imgNode.prop('crossOrigin')).toBe(file.crossOrigin);
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should not exist crossorigin attribute when does not set file.crossorigin in case of listType="picture-card"', () => {
|
||||
const list = [
|
||||
{
|
||||
uid: '0',
|
||||
name: 'xxx.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = mount(
|
||||
<Upload fileList={list} listType="picture">
|
||||
<button type="button">upload</button>
|
||||
</Upload>,
|
||||
);
|
||||
list.forEach((file, i) => {
|
||||
const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img').at(i);
|
||||
expect(imgNode.prop('crossOrigin')).toBe(undefined);
|
||||
expect(imgNode.prop('crossOrigin')).toBe(file.crossOrigin);
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should exist crossorigin attribute when set file.crossorigin in case of listType="picture-card"', () => {
|
||||
const list = [
|
||||
{
|
||||
uid: '0',
|
||||
name: 'xxx.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
|
||||
crossOrigin: '',
|
||||
},
|
||||
{
|
||||
uid: '1',
|
||||
name: 'xxx.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
{
|
||||
uid: '2',
|
||||
name: 'xxx.png',
|
||||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png',
|
||||
crossOrigin: 'use-credentials',
|
||||
},
|
||||
];
|
||||
|
||||
const wrapper = mount(
|
||||
<Upload fileList={list} listType="picture">
|
||||
<button type="button">upload</button>
|
||||
</Upload>,
|
||||
);
|
||||
list.forEach((file, i) => {
|
||||
const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img').at(i);
|
||||
expect(imgNode.prop('crossOrigin')).not.toBe(undefined);
|
||||
expect(imgNode.prop('crossOrigin')).toBe(file.crossOrigin);
|
||||
});
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
@ -60,6 +60,7 @@ Extends File with additional props.
|
||||
| thumbUrl | Thumb image url | string | - |
|
||||
| uid | unique id. Will auto generate when not provided | string | - |
|
||||
| url | Download url | string | - |
|
||||
| crossOrigin | CORS settings attributes | `'anonymous'` \| `'use-credentials'` \| `''` | - |
|
||||
|
||||
### onChange
|
||||
|
||||
|
@ -61,6 +61,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg
|
||||
| thumbUrl | 缩略图地址 | string | - |
|
||||
| uid | 唯一标识符,不设置时会自动生成 | string | - |
|
||||
| url | 下载地址 | string | - |
|
||||
| crossOrigin | CORS 属性设置 | `'anonymous'` \| `'use-credentials'` \| `''` | - |
|
||||
|
||||
### onChange
|
||||
|
||||
|
@ -27,6 +27,7 @@ export interface UploadFile<T = any> {
|
||||
status?: UploadFileStatus;
|
||||
percent?: number;
|
||||
thumbUrl?: string;
|
||||
crossOrigin?: React.ImgHTMLAttributes<HTMLImageElement>['crossOrigin'];
|
||||
originFileObj?: RcFile;
|
||||
response?: T;
|
||||
error?: any;
|
||||
|
@ -129,8 +129,8 @@
|
||||
"rc-dialog": "~8.7.0",
|
||||
"rc-drawer": "~4.4.2",
|
||||
"rc-dropdown": "~3.4.0",
|
||||
"rc-field-form": "~1.25.0",
|
||||
"rc-image": "~5.4.0",
|
||||
"rc-field-form": "~1.26.1",
|
||||
"rc-image": "~5.5.0",
|
||||
"rc-input": "~0.0.1-alpha.5",
|
||||
"rc-input-number": "~7.3.0",
|
||||
"rc-mentions": "~1.7.0",
|
||||
|
Loading…
Reference in New Issue
Block a user