diff --git a/components/date-picker/generatePicker/generateRangePicker.tsx b/components/date-picker/generatePicker/generateRangePicker.tsx index 222dcfd1df..a986cd5abb 100644 --- a/components/date-picker/generatePicker/generateRangePicker.tsx +++ b/components/date-picker/generatePicker/generateRangePicker.tsx @@ -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( generateConfig: GenerateConfig, @@ -42,7 +43,7 @@ export default function generateRangePicker( 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( ...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( [`${prefixCls}-borderless`]: !bordered, }, getStatusClassNames( - prefixCls, + prefixCls as string, getMergedStatus(contextStatus, customStatus), hasFeedback, ), @@ -136,5 +136,12 @@ export default function generateRangePicker( } } - return RangePicker; + return forwardRef>((props, ref) => { + const { prefixCls: customizePrefixCls } = props; + + const { getPrefixCls } = useContext(ConfigContext); + const prefixCls = getPrefixCls('picker', customizePrefixCls); + + return ; + }) as unknown as PickerComponentClass>; } diff --git a/components/date-picker/generatePicker/generateSinglePicker.tsx b/components/date-picker/generatePicker/generateSinglePicker.tsx index 6fbae02e3d..81278515b2 100644 --- a/components/date-picker/generatePicker/generateSinglePicker.tsx +++ b/components/date-picker/generatePicker/generateSinglePicker.tsx @@ -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(generateConfig: GenerateConfig) { type DatePickerProps = PickerProps & { @@ -67,7 +68,7 @@ export default function generatePicker(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(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(generateConfig: GenerateConfig< [`${prefixCls}-borderless`]: !bordered, }, getStatusClassNames( - prefixCls, + prefixCls as string, getMergedStatus(contextStatus, customStatus), hasFeedback, ), @@ -167,11 +167,26 @@ export default function generatePicker(generateConfig: GenerateConfig< } } + const PickerWrapper = forwardRef, InnerPickerProps>((props, ref) => { + const { prefixCls: customizePrefixCls } = props; + + const { getPrefixCls } = useContext(ConfigContext); + const prefixCls = getPrefixCls('picker', customizePrefixCls); + + const pickerProps: InnerPickerProps = { + ...props, + prefixCls, + ref, + }; + + return ; + }); + if (displayName) { - Picker.displayName = displayName; + PickerWrapper.displayName = displayName; } - return Picker as PickerComponentClass; + return PickerWrapper as unknown as PickerComponentClass; } const DatePicker = getPicker(); diff --git a/components/form/Form.tsx b/components/form/Form.tsx index a21184229d..9979ed16fe 100644 --- a/components/form/Form.tsx +++ b/components/form/Form.tsx @@ -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 = (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(InternalForm) as > & { ref?: React.Ref> }, ) => React.ReactElement; -export { useForm, List, FormInstance }; +export { useForm, List, FormInstance, useWatch }; export default Form; diff --git a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap index 38f80970e8..03ff3d63d6 100644 --- a/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/form/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -14850,6 +14850,157 @@ exports[`renders ./components/form/demo/time-related-controls.md extend context `; +exports[`renders ./components/form/demo/useWatch.md extend context correctly 1`] = ` +Array [ +
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + +
+
+ +
+
+
+
+
+
+
, +
+
+      Name Value: 
+    
+
, +] +`; + exports[`renders ./components/form/demo/validate-other.md extend context correctly 1`] = `
`; +exports[`renders ./components/form/demo/useWatch.md correctly 1`] = ` +Array [ + +
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + +
+
+ +
+
+
+
+
+
+
, +
+
+      Name Value: 
+    
+
, +] +`; + exports[`renders ./components/form/demo/validate-other.md correctly 1`] = `
{ 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 ( + + + + ); + }; + + render(); + expect(subFormInstance).toBe(formInstance); + }); }); diff --git a/components/form/context.tsx b/components/form/context.tsx index b355d47ab4..4ab398786b 100644 --- a/components/form/context.tsx +++ b/components/form/context.tsx @@ -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({ diff --git a/components/form/demo/useWatch.md b/components/form/demo/useWatch.md new file mode 100644 index 0000000000..6184343fc2 --- /dev/null +++ b/components/form/demo/useWatch.md @@ -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 ( + <> +
+ + + + + + +
+ + +
Name Value: {nameValue}
+
+ + ); +}; + +export default Demo; +``` diff --git a/components/form/hooks/useFormInstance.ts b/components/form/hooks/useFormInstance.ts new file mode 100644 index 0000000000..c83c024620 --- /dev/null +++ b/components/form/hooks/useFormInstance.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import { FormContext } from '../context'; +import { FormInstance } from './useForm'; + +export default function useFormInstance(): FormInstance { + const { form } = useContext(FormContext); + + return form!; +} diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md index 6a4f73b38b..adb4a1ba52 100644 --- a/components/form/index.en-US.md +++ b/components/form/index.en-US.md @@ -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 + , + ); + 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( + + + , + ); + 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( + + + , + ); + 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( + + + , + ); + 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(); + }); }); diff --git a/components/upload/index.en-US.md b/components/upload/index.en-US.md index 5c03abfb57..325dbac878 100644 --- a/components/upload/index.en-US.md +++ b/components/upload/index.en-US.md @@ -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 diff --git a/components/upload/index.zh-CN.md b/components/upload/index.zh-CN.md index a4ca5e7bf8..79cd9e9eeb 100644 --- a/components/upload/index.zh-CN.md +++ b/components/upload/index.zh-CN.md @@ -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 diff --git a/components/upload/interface.tsx b/components/upload/interface.tsx index e523884381..9e5d20705e 100755 --- a/components/upload/interface.tsx +++ b/components/upload/interface.tsx @@ -27,6 +27,7 @@ export interface UploadFile { status?: UploadFileStatus; percent?: number; thumbUrl?: string; + crossOrigin?: React.ImgHTMLAttributes['crossOrigin']; originFileObj?: RcFile; response?: T; error?: any; diff --git a/package.json b/package.json index b83a26b5e4..8b8cbd0c22 100644 --- a/package.json +++ b/package.json @@ -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",