diff --git a/components/_util/hooks/useSize.ts b/components/_util/hooks/useSize.ts index 6b9d8ee87..c3bbf0612 100644 --- a/components/_util/hooks/useSize.ts +++ b/components/_util/hooks/useSize.ts @@ -13,11 +13,13 @@ const useProvideSize = (props: Record): ComputedRef = return size; }; -const useInjectSize = (): ComputedRef => { - const size: ComputedRef = inject( - sizeProvider, - computed(() => ('default' as unknown) as T), - ); +const useInjectSize = (props?: Record): ComputedRef => { + const size: ComputedRef = props + ? computed(() => props.size) + : inject( + sizeProvider, + computed(() => ('default' as unknown) as T), + ); return size; }; diff --git a/components/form/ErrorList.tsx b/components/form/ErrorList.tsx index c6423c271..99b35bd86 100644 --- a/components/form/ErrorList.tsx +++ b/components/form/ErrorList.tsx @@ -1,8 +1,9 @@ import { useInjectFormItemPrefix } from './context'; import { VueNode } from '../_util/type'; -import { computed, defineComponent, ref, watch } from '@vue/runtime-core'; +import { defineComponent, ref, watch } from '@vue/runtime-core'; import classNames from '../_util/classNames'; import Transition, { getTransitionProps } from '../_util/transition'; +import useConfigInject from '../_util/hooks/useConfigInject'; export interface ErrorListProps { errors?: VueNode[]; @@ -12,29 +13,53 @@ export interface ErrorListProps { onDomErrorVisibleChange?: (visible: boolean) => void; } -export default defineComponent({ +export default defineComponent({ name: 'ErrorList', + props: ['errors', 'help', 'onDomErrorVisibleChange'], setup(props) { + const { prefixCls: rootPrefixCls } = useConfigInject('', props); const { prefixCls, status } = useInjectFormItemPrefix(); - const visible = computed(() => props.errors && props.errors.length); + const visible = ref(!!(props.errors && props.errors.length)); const innerStatus = ref(status.value); + let timeout = ref(); + watch([() => props.errors, () => props.help], () => { + window.clearTimeout(timeout.value); + if (props.help) { + visible.value = !!(props.errors && props.errors.length); + } else { + timeout.value = window.setTimeout(() => { + visible.value = !!(props.errors && props.errors.length); + }); + } + }); // Memo status in same visible - watch([() => visible, () => status], () => { + watch([visible, status], () => { if (visible.value && status.value) { innerStatus.value = status.value; } }); + watch( + visible, + () => { + if (visible.value) { + props.onDomErrorVisibleChange?.(true); + } + }, + { immediate: true, flush: 'post' }, + ); return () => { const baseClassName = `${prefixCls.value}-item-explain`; - const transitionProps = getTransitionProps('show-help', { - onAfterLeave: () => props.onDomErrorVisibleChange?.(false), + const transitionProps = getTransitionProps(`${rootPrefixCls.value}-show-help`, { + onAfterLeave: () => { + props.onDomErrorVisibleChange?.(false); + }, }); return ( - {visible ? ( + {visible.value ? (
diff --git a/components/form/Form.tsx b/components/form/Form.tsx index 5185a0c6a..a8e99a851 100755 --- a/components/form/Form.tsx +++ b/components/form/Form.tsx @@ -1,18 +1,16 @@ import { defineComponent, - inject, - provide, PropType, computed, ExtractPropTypes, HTMLAttributes, + watch, + ref, } from 'vue'; import PropTypes from '../_util/vue-types'; import classNames from '../_util/classNames'; import warning from '../_util/warning'; -import FormItem from './FormItem'; -import { getSlot } from '../_util/props-util'; -import { defaultConfigProvider, SizeType } from '../config-provider'; +import FormItem, { FieldExpose } from './FormItem'; import { getNamePath, containsNamePath } from './utils/valueUtil'; import { defaultValidateMessages } from './utils/messages'; import { allPromiseFinish } from './utils/asyncUtil'; @@ -23,6 +21,10 @@ import initDefaultProps from '../_util/props-util/initDefaultProps'; import { tuple, VueNode } from '../_util/type'; import { ColProps } from '../grid/Col'; import { InternalNamePath, NamePath, ValidateErrorEntity, ValidateOptions } from './interface'; +import { useInjectSize } from '../_util/hooks/useSize'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import { useProvideForm } from './context'; +import { SizeType } from '../config-provider'; export type RequiredMark = boolean | 'optional'; export type FormLayout = 'horizontal' | 'inline' | 'vertical'; @@ -61,7 +63,7 @@ export const formProps = { colon: PropTypes.looseBool, labelAlign: PropTypes.oneOf(tuple('left', 'right')), prefixCls: PropTypes.string, - requiredMark: { type: [String, Boolean] as PropType }, + requiredMark: { type: [String, Boolean] as PropType, default: undefined }, /** @deprecated Will warning in future branch. Pls use `requiredMark` instead. */ hideRequiredMark: PropTypes.looseBool, model: PropTypes.object, @@ -93,92 +95,88 @@ const Form = defineComponent({ colon: true, }), Item: FormItem, - setup(props) { - return { - configProvider: inject('configProvider', defaultConfigProvider), - fields: [], - form: undefined, - lastValidatePromise: null, - vertical: computed(() => props.layout === 'vertical'), - }; - }, - watch: { - rules() { - if (this.validateOnRuleChange) { - this.validateFields(); + emits: ['finishFailed', 'submit', 'finish'], + setup(props, { emit, slots, expose }) { + const size = useInjectSize(props); + const { prefixCls, direction, form: contextForm } = useConfigInject('form', props); + const requiredMark = computed(() => props.requiredMark === '' || props.requiredMark); + const mergedRequiredMark = computed(() => { + if (requiredMark.value !== undefined) { + return requiredMark.value; } - }, - }, - created() { - provide('FormContext', this); - }, - methods: { - addField(field: any) { - if (field) { - this.fields.push(field); + + if (contextForm && contextForm.value?.requiredMark !== undefined) { + return contextForm.value.requiredMark; } - }, - removeField(field: any) { - if (field.fieldName) { - this.fields.splice(this.fields.indexOf(field), 1); + + if (props.hideRequiredMark) { + return false; } - }, - handleSubmit(e: Event) { - e.preventDefault(); - e.stopPropagation(); - this.$emit('submit', e); - const res = this.validateFields(); - res - .then(values => { - this.$emit('finish', values); - }) - .catch(errors => { - this.handleFinishFailed(errors); - }); - }, - getFieldsByNameList(nameList: NamePath) { + return true; + }); + + const formClassName = computed(() => + classNames(prefixCls.value, { + [`${prefixCls.value}-${props.layout}`]: true, + [`${prefixCls.value}-hide-required-mark`]: mergedRequiredMark.value === false, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [`${prefixCls.value}-${size.value}`]: size.value, + }), + ); + const lastValidatePromise = ref(); + const fields: Record = {}; + + const addField = (eventKey: string, field: FieldExpose) => { + fields[eventKey] = field; + }; + const removeField = (eventKey: string) => { + delete fields[eventKey]; + }; + + const getFieldsByNameList = (nameList: NamePath) => { const provideNameList = !!nameList; const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : []; if (!provideNameList) { - return this.fields; + return Object.values(fields); } else { - return this.fields.filter( - field => namePathList.findIndex(namePath => isEqualName(namePath, field.fieldName)) > -1, + return Object.values(fields).filter( + field => + namePathList.findIndex(namePath => isEqualName(namePath, field.fieldName.value)) > -1, ); } - }, - resetFields(name: NamePath) { - if (!this.model) { + }; + const resetFields = (name: NamePath) => { + if (!props.model) { warning(false, 'Form', 'model is required for resetFields to work.'); return; } - this.getFieldsByNameList(name).forEach(field => { + getFieldsByNameList(name).forEach(field => { field.resetField(); }); - }, - clearValidate(name: NamePath) { - this.getFieldsByNameList(name).forEach(field => { + }; + const clearValidate = (name: NamePath) => { + getFieldsByNameList(name).forEach(field => { field.clearValidate(); }); - }, - handleFinishFailed(errorInfo: ValidateErrorEntity) { - const { scrollToFirstError } = this; - this.$emit('finishFailed', errorInfo); + }; + const handleFinishFailed = (errorInfo: ValidateErrorEntity) => { + const { scrollToFirstError } = props; + emit('finishFailed', errorInfo); if (scrollToFirstError && errorInfo.errorFields.length) { let scrollToFieldOptions: Options = {}; if (typeof scrollToFirstError === 'object') { scrollToFieldOptions = scrollToFirstError; } - this.scrollToField(errorInfo.errorFields[0].name, scrollToFieldOptions); + scrollToField(errorInfo.errorFields[0].name, scrollToFieldOptions); } - }, - validate(...args: any[]) { - return this.validateField(...args); - }, - scrollToField(name: NamePath, options = {}) { - const fields = this.getFieldsByNameList(name); + }; + const validate = (...args: any[]) => { + return validateField(...args); + }; + const scrollToField = (name: NamePath, options = {}) => { + const fields = getFieldsByNameList(name); if (fields.length) { - const fieldId = fields[0].fieldId; + const fieldId = fields[0].fieldId.value; const node = fieldId ? document.getElementById(fieldId) : null; if (node) { @@ -189,12 +187,12 @@ const Form = defineComponent({ }); } } - }, + }; // eslint-disable-next-line no-unused-vars - getFieldsValue(nameList: NamePath[] | true = true) { + const getFieldsValue = (nameList: NamePath[] | true = true) => { const values: any = {}; - this.fields.forEach(({ fieldName, fieldValue }) => { - values[fieldName] = fieldValue; + Object.values(fields).forEach(({ fieldName, fieldValue }) => { + values[fieldName.value] = fieldValue.value; }); if (nameList === true) { return values; @@ -205,14 +203,14 @@ const Form = defineComponent({ ); return res; } - }, - validateFields(nameList?: NamePath[], options?: ValidateOptions) { + }; + const validateFields = (nameList?: NamePath[], options?: ValidateOptions) => { warning( !(nameList instanceof Function), 'Form', 'validateFields/validateField/validate not support callback, please use promise instead', ); - if (!this.model) { + if (!props.model) { warning(false, 'Form', 'model is required for validateFields to work.'); return Promise.reject('Form `model` is required for validateFields to work.'); } @@ -227,25 +225,25 @@ const Form = defineComponent({ errors: string[]; }>[] = []; - this.fields.forEach(field => { + Object.values(fields).forEach(field => { // Add field if not provide `nameList` if (!provideNameList) { - namePathList.push(field.getNamePath()); + namePathList.push(field.namePath.value); } // Skip if without rule - if (!field.getRules().length) { + if (!field.rules?.value.length) { return; } - const fieldNamePath = field.getNamePath(); + const fieldNamePath = field.namePath.value; // Add field validate rule in to promise list if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) { const promise = field.validateRules({ validateMessages: { ...defaultValidateMessages, - ...this.validateMessages, + ...props.validateMessages, }, ...options, }); @@ -265,21 +263,21 @@ const Form = defineComponent({ }); const summaryPromise = allPromiseFinish(promiseList); - this.lastValidatePromise = summaryPromise; + lastValidatePromise.value = summaryPromise; const returnPromise = summaryPromise .then(() => { - if (this.lastValidatePromise === summaryPromise) { - return Promise.resolve(this.getFieldsValue(namePathList)); + if (lastValidatePromise.value === summaryPromise) { + return Promise.resolve(getFieldsValue(namePathList)); } return Promise.reject([]); }) .catch(results => { const errorList = results.filter(result => result && result.errors.length); return Promise.reject({ - values: this.getFieldsValue(namePathList), + values: getFieldsValue(namePathList), errorFields: errorList, - outOfDate: this.lastValidatePromise !== summaryPromise, + outOfDate: lastValidatePromise.value !== summaryPromise, }); }); @@ -287,29 +285,65 @@ const Form = defineComponent({ returnPromise.catch(e => e); return returnPromise; - }, - validateField(...args: any[]) { - return this.validateFields(...args); - }, - }, + }; + const validateField = (...args: any[]) => { + return validateFields(...args); + }; - render() { - const { prefixCls: customizePrefixCls, hideRequiredMark, layout, handleSubmit, size } = this; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('form', customizePrefixCls); - const { class: className, ...restProps } = this.$attrs; + const handleSubmit = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + emit('submit', e); + const res = validateFields(); + res + .then(values => { + emit('finish', values); + }) + .catch(errors => { + handleFinishFailed(errors); + }); + }; - const formClassName = classNames(prefixCls, className, { - [`${prefixCls}-${layout}`]: true, - // [`${prefixCls}-rtl`]: direction === 'rtl', - [`${prefixCls}-${size}`]: size, - [`${prefixCls}-hide-required-mark`]: hideRequiredMark, + expose({ + resetFields, + clearValidate, + validateFields, + getFieldsValue, + validate, + scrollToField, + }); + + useProvideForm({ + model: computed(() => props.model), + name: computed(() => props.name), + labelAlign: computed(() => props.labelAlign), + labelCol: computed(() => props.labelCol), + wrapperCol: computed(() => props.wrapperCol), + vertical: computed(() => props.layout === 'vertical'), + colon: computed(() => props.colon), + requiredMark: mergedRequiredMark, + validateTrigger: computed(() => props.validateTrigger), + rules: computed(() => props.rules), + addField, + removeField, }); - return ( -
- {getSlot(this)} -
+ + watch( + () => props.rules, + () => { + if (props.validateOnRuleChange) { + validateFields(); + } + }, ); + + return () => { + return ( +
+ {slots.default?.()} +
+ ); + }; }, }); diff --git a/components/form/FormItem.tsx b/components/form/FormItem.tsx index becb1d165..194edc435 100644 --- a/components/form/FormItem.tsx +++ b/components/form/FormItem.tsx @@ -1,50 +1,47 @@ import { - inject, - provide, PropType, defineComponent, computed, nextTick, ExtractPropTypes, + ref, + watchEffect, + onBeforeUnmount, + ComputedRef, } from 'vue'; import cloneDeep from 'lodash-es/cloneDeep'; import PropTypes from '../_util/vue-types'; -import classNames from '../_util/classNames'; -import { getTransitionProps, Transition } from '../_util/transition'; import Row from '../grid/Row'; -import Col, { ColProps } from '../grid/Col'; -import hasProp, { - findDOMNode, - getComponent, - getOptionProps, - getEvents, - isValidElement, - getSlot, -} from '../_util/props-util'; +import { ColProps } from '../grid/Col'; +import { isValidElement, flattenChildren } from '../_util/props-util'; import BaseMixin from '../_util/BaseMixin'; -import { defaultConfigProvider } from '../config-provider'; import { cloneElement } from '../_util/vnode'; -import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; -import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled'; -import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; -import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; -import { validateRules } from './utils/validateUtil'; +import { validateRules as validateRulesUtil } from './utils/validateUtil'; import { getNamePath } from './utils/valueUtil'; import { toArray } from './utils/typeUtil'; import { warning } from '../vc-util/warning'; import find from 'lodash-es/find'; -import { tuple, VueNode } from '../_util/type'; -import { ValidateOptions } from './interface'; +import { tuple } from '../_util/type'; +import { InternalNamePath, RuleObject, ValidateOptions } from './interface'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import { useInjectForm } from './context'; +import FormItemLabel from './FormItemLabel'; +import FormItemInput from './FormItemInput'; +import { ValidationRule } from './Form'; const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', ''); export type ValidateStatus = typeof ValidateStatuses[number]; -const iconMap = { - success: CheckCircleFilled, - warning: ExclamationCircleFilled, - error: CloseCircleFilled, - validating: LoadingOutlined, -}; +export interface FieldExpose { + fieldValue: ComputedRef; + fieldId: ComputedRef; + fieldName: ComputedRef; + resetField: () => void; + clearValidate: () => void; + namePath: ComputedRef; + rules?: ComputedRef; + validateRules: (options: ValidateOptions) => Promise | Promise; +} function getPropByPath(obj: any, namePathList: any, strict?: boolean) { let tempObj = obj; @@ -103,15 +100,25 @@ export const formItemProps = { export type FormItemProps = Partial>; +let indexGuid = 0; export default defineComponent({ name: 'AFormItem', mixins: [BaseMixin], inheritAttrs: false, __ANT_NEW_FORM_ITEM: true, props: formItemProps, - setup(props) { - const FormContext = inject('FormContext', {}) as any; + slots: ['help', 'label', 'extra'], + setup(props, { slots }) { + warning(props.prop === undefined, `\`prop\` is deprecated. Please use \`name\` instead.`); + const eventKey = `form-item-${++indexGuid}`; + const { prefixCls } = useConfigInject('form', props); + const formContext = useInjectForm(); const fieldName = computed(() => props.name || props.prop); + const errors = ref([]); + const validateMessage = ref(''); + const validateDisabled = ref(false); + const domErrorVisible = ref(false); + const inputRef = ref(); const namePath = computed(() => { const val = fieldName.value; return getNamePath(val); @@ -123,26 +130,30 @@ export default defineComponent({ } else if (!namePath.value.length) { return undefined; } else { - const formName = FormContext.name; + const formName = formContext.name.value; const mergedId = namePath.value.join('_'); return formName ? `${formName}_${mergedId}` : mergedId; } }); const fieldValue = computed(() => { - const model = FormContext.model; + const model = formContext.model.value; if (!model || !fieldName.value) { return; } return getPropByPath(model, namePath.value, true).v; }); + + const initialValue = ref(cloneDeep(fieldValue.value)); const mergedValidateTrigger = computed(() => { let validateTrigger = - props.validateTrigger !== undefined ? props.validateTrigger : FormContext.validateTrigger; + props.validateTrigger !== undefined + ? props.validateTrigger + : formContext.validateTrigger.value; validateTrigger = validateTrigger === undefined ? 'change' : validateTrigger; return toArray(validateTrigger); }); - const getRules = () => { - let formRules = FormContext.rules; + const rulesRef = computed(() => { + let formRules = formContext.rules.value; const selfRules = props.rules; const requiredRule = props.required !== undefined @@ -156,9 +167,9 @@ export default defineComponent({ } else { return rules.concat(requiredRule); } - }; + }); const isRequired = computed(() => { - const rules = getRules(); + const rules = rulesRef.value; let isRequired = false; if (rules && rules.length) { rules.every(rule => { @@ -171,360 +182,234 @@ export default defineComponent({ } return isRequired || props.required; }); - return { - isFormItemChildren: inject('isFormItemChildren', false), - configProvider: inject('configProvider', defaultConfigProvider), - FormContext, - fieldId, - fieldName, - namePath, - isRequired, - getRules, - fieldValue, - mergedValidateTrigger, - }; - }, - data() { - warning(!hasProp(this, 'prop'), `\`prop\` is deprecated. Please use \`name\` instead.`); - return { - validateState: this.validateStatus, - validateMessage: '', - validateDisabled: false, - validator: {}, - helpShow: false, - errors: [], - initialValue: undefined, - }; - }, - watch: { - validateStatus(val) { - this.validateState = val; - }, - }, - created() { - provide('isFormItemChildren', true); - }, - mounted() { - if (this.fieldName) { - const { addField } = this.FormContext; - addField && addField(this); - this.initialValue = cloneDeep(this.fieldValue); - } - }, - beforeUnmount() { - const { removeField } = this.FormContext; - removeField && removeField(this); - }, - methods: { - getNamePath() { - const { fieldName } = this; - const { prefixName = [] } = this.FormContext; - return fieldName !== undefined ? [...prefixName, ...this.namePath] : []; - }, - validateRules(options: ValidateOptions) { - const { validateFirst = false, messageVariables } = this.$props; + const validateState = ref(); + watchEffect(() => { + validateState.value = props.validateStatus; + }); + + const validateRules = (options: ValidateOptions) => { + const { validateFirst = false, messageVariables } = props; const { triggerName } = options || {}; - const namePath = this.getNamePath(); - let filteredRules = this.getRules(); + let filteredRules = rulesRef.value; if (triggerName) { filteredRules = filteredRules.filter(rule => { const { trigger } = rule; - if (!trigger && !this.mergedValidateTrigger.length) { + if (!trigger && !mergedValidateTrigger.value.length) { return true; } - const triggerList = toArray(trigger || this.mergedValidateTrigger); + const triggerList = toArray(trigger || mergedValidateTrigger.value); return triggerList.includes(triggerName); }); } if (!filteredRules.length) { return Promise.resolve(); } - const promise = validateRules( - namePath, - this.fieldValue, - filteredRules, + const promise = validateRulesUtil( + namePath.value, + fieldValue.value, + filteredRules as RuleObject[], options, validateFirst, messageVariables, ); - this.validateState = 'validating'; - this.errors = []; + validateState.value = 'validating'; + errors.value = []; promise .catch(e => e) - .then((errors = []) => { - if (this.validateState === 'validating') { - this.validateState = errors.length ? 'error' : 'success'; - this.validateMessage = errors[0]; - this.errors = errors; + .then((ers = []) => { + if (validateState.value === 'validating') { + validateState.value = ers.length ? 'error' : 'success'; + validateMessage.value = ers[0]; + errors.value = ers; } }); return promise; - }, - onFieldBlur() { - this.validateRules({ triggerName: 'blur' }); - }, - onFieldChange() { - if (this.validateDisabled) { - this.validateDisabled = false; + }; + + const onFieldBlur = () => { + validateRules({ triggerName: 'blur' }); + }; + const onFieldChange = () => { + if (validateDisabled.value) { + validateDisabled.value = false; return; } - this.validateRules({ triggerName: 'change' }); - }, - clearValidate() { - this.validateState = ''; - this.validateMessage = ''; - this.validateDisabled = false; - }, - resetField() { - this.validateState = ''; - this.validateMessage = ''; - const model = this.FormContext.model || {}; - const value = this.fieldValue; - const prop = getPropByPath(model, this.namePath, true); - this.validateDisabled = true; + validateRules({ triggerName: 'change' }); + }; + const clearValidate = () => { + validateState.value = ''; + validateMessage.value = ''; + validateDisabled.value = false; + }; + + const resetField = () => { + validateState.value = ''; + validateMessage.value = ''; + const model = formContext.model.value || {}; + const value = fieldValue.value; + const prop = getPropByPath(model, namePath.value, true); + validateDisabled.value = true; if (Array.isArray(value)) { - prop.o[prop.k] = [].concat(this.initialValue); + prop.o[prop.k] = [].concat(initialValue.value); } else { - prop.o[prop.k] = this.initialValue; + prop.o[prop.k] = initialValue.value; } // reset validateDisabled after onFieldChange triggered nextTick(() => { - this.validateDisabled = false; + validateDisabled.value = false; }); - }, - getHelpMessage() { - const help = getComponent(this, 'help'); - - return this.validateMessage || help; - }, + }; - onLabelClick() { - const id = this.fieldId; - if (!id) { + const onLabelClick = () => { + const id = fieldId.value; + if (!id || !inputRef.value) { return; } - const formItemNode = findDOMNode(this); - const control = formItemNode.querySelector(`[id="${id}"]`); + const control = inputRef.value.$el.querySelector(`[id="${id}"]`); if (control && control.focus) { control.focus(); } - }, - - onHelpAnimEnd(_key: string, helpShow: boolean) { - this.helpShow = helpShow; - if (!helpShow) { - this.$forceUpdate(); - } - }, - - renderHelp(prefixCls: string) { - const help = this.getHelpMessage(); - const children = help ? ( -
- {help} -
- ) : null; - if (children) { - this.helpShow = !!children; - } - const transitionProps = getTransitionProps('show-help', { - onAfterEnter: () => this.onHelpAnimEnd('help', true), - onAfterLeave: () => this.onHelpAnimEnd('help', false), - }); - return ( - - {children} - - ); - }, - - renderExtra(prefixCls: string) { - const extra = getComponent(this, 'extra'); - return extra ?
{extra}
: null; - }, - - renderValidateWrapper(prefixCls: string, c1: VueNode, c2: VueNode, c3: VueNode) { - const validateStatus = this.validateState; + }; + formContext.addField(eventKey, { + fieldValue, + fieldId, + fieldName, + resetField, + clearValidate, + namePath, + validateRules, + rules: rulesRef, + }); + onBeforeUnmount(() => { + formContext.removeField(eventKey); + }); + // const onHelpAnimEnd = (_key: string, helpShow: boolean) => { + // this.helpShow = helpShow; + // if (!helpShow) { + // this.$forceUpdate(); + // } + // }; + const itemClassName = computed(() => ({ + [`${prefixCls.value}-item`]: true, - let classes = `${prefixCls}-item-control`; - if (validateStatus) { - classes = classNames(`${prefixCls}-item-control`, { - 'has-feedback': validateStatus && this.hasFeedback, - 'has-success': validateStatus === 'success', - 'has-warning': validateStatus === 'warning', - 'has-error': validateStatus === 'error', - 'is-validating': validateStatus === 'validating', + // Status + [`${prefixCls.value}-item-has-feedback`]: validateState.value && props.hasFeedback, + [`${prefixCls.value}-item-has-success`]: validateState.value === 'success', + [`${prefixCls.value}-item-has-warning`]: validateState.value === 'warning', + [`${prefixCls.value}-item-has-error`]: validateState.value === 'error', + [`${prefixCls.value}-item-is-validating`]: validateState.value === 'validating', + [`${prefixCls.value}-item-hidden`]: props.hidden, + })); + return () => { + const help = props.help ?? slots.help?.(); + const children = flattenChildren(slots.default?.()); + let firstChildren = children[0]; + if (fieldName.value && props.autoLink && isValidElement(firstChildren)) { + const originalEvents = firstChildren.props; + const originalBlur = originalEvents.onBlur; + const originalChange = originalEvents.onChange; + firstChildren = cloneElement(firstChildren, { + ...(fieldId.value ? { id: fieldId.value } : undefined), + onBlur: (...args: any[]) => { + if (Array.isArray(originalChange)) { + for (let i = 0, l = originalChange.length; i < l; i++) { + originalBlur[i](...args); + } + } else if (originalBlur) { + originalBlur(...args); + } + onFieldBlur(); + }, + onChange: (...args: any[]) => { + if (Array.isArray(originalChange)) { + for (let i = 0, l = originalChange.length; i < l; i++) { + originalChange[i](...args); + } + } else if (originalChange) { + originalChange(...args); + } + onFieldChange(); + }, }); } - const IconNode = validateStatus && iconMap[validateStatus]; - - const icon = - this.hasFeedback && IconNode ? ( - - - - ) : null; return ( -
- - {c1} - {icon} - - {c2} - {c3} -
- ); - }, - - renderWrapper(prefixCls: string, children: VueNode) { - const { wrapperCol: contextWrapperCol } = (this.isFormItemChildren - ? {} - : this.FormContext) as any; - const { wrapperCol } = this; - const mergedWrapperCol = wrapperCol || contextWrapperCol || {}; - const { style, id, ...restProps } = mergedWrapperCol; - const className = classNames(`${prefixCls}-item-control`, mergedWrapperCol.class); - const colProps = { - ...restProps, - class: className, - key: 'wrapper', - style, - id, - }; - return {children}; - }, - - renderLabel(prefixCls: string) { - const { - vertical, - labelAlign: contextLabelAlign, - labelCol: contextLabelCol, - colon: contextColon, - } = this.FormContext; - const { labelAlign, labelCol, colon, fieldId, htmlFor } = this; - const label = getComponent(this, 'label'); - const required = this.isRequired; - const mergedLabelCol = labelCol || contextLabelCol || {}; - - const mergedLabelAlign = labelAlign || contextLabelAlign; - const labelClsBasic = `${prefixCls}-item-label`; - const labelColClassName = classNames( - labelClsBasic, - mergedLabelAlign === 'left' && `${labelClsBasic}-left`, - mergedLabelCol.class, - ); - const { - class: labelColClass, - style: labelColStyle, - id: labelColId, - ...restProps - } = mergedLabelCol; - let labelChildren = label; - // Keep label is original where there should have no colon - const computedColon = colon === true || (contextColon !== false && colon !== false); - const haveColon = computedColon && !vertical; - // Remove duplicated user input colon - if (haveColon && typeof label === 'string' && label.trim() !== '') { - labelChildren = label.replace(/[::]\s*$/, ''); - } - - const labelClassName = classNames({ - [`${prefixCls}-item-required`]: required, - [`${prefixCls}-item-no-colon`]: !computedColon, - }); - const colProps = { - ...restProps, - class: labelColClassName, - key: 'label', - style: labelColStyle, - id: labelColId, - }; - - return label ? ( - - - - ) : null; - }, - renderChildren(prefixCls: string, child: VueNode) { - return [ - this.renderLabel(prefixCls), - this.renderWrapper( - prefixCls, - this.renderValidateWrapper( - prefixCls, - child, - this.renderHelp(prefixCls), - this.renderExtra(prefixCls), - ), - ), - ]; - }, - renderFormItem(child: any[]) { - const validateStatus = this.validateState; - const { prefixCls: customizePrefixCls, hidden, hasFeedback } = this.$props; - const { class: className, ...restProps } = this.$attrs as any; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('form', customizePrefixCls); - const children = this.renderChildren(prefixCls, child); - const itemClassName = { - [className]: className, - [`${prefixCls}-item`]: true, - [`${prefixCls}-item-with-help`]: this.helpShow, - - // Status - [`${prefixCls}-item-has-feedback`]: validateStatus && hasFeedback, - [`${prefixCls}-item-has-success`]: validateStatus === 'success', - [`${prefixCls}-item-has-warning`]: validateStatus === 'warning', - [`${prefixCls}-item-has-error`]: validateStatus === 'error', - [`${prefixCls}-item-is-validating`]: validateStatus === 'validating', - [`${prefixCls}-item-hidden`]: hidden, - }; - - return ( - - {children} + {[firstChildren, children.slice(1)]} + ); - }, - }, - render() { - const { autoLink } = getOptionProps(this); - const children = getSlot(this); - let firstChildren = children[0]; - if (this.fieldName && autoLink && isValidElement(firstChildren)) { - const originalEvents = getEvents(firstChildren); - const originalBlur = originalEvents.onBlur; - const originalChange = originalEvents.onChange; - firstChildren = cloneElement(firstChildren, { - ...(this.fieldId ? { id: this.fieldId } : undefined), - onBlur: (...args: any[]) => { - originalBlur && originalBlur(...args); - this.onFieldBlur(); - }, - onChange: (...args: any[]) => { - if (Array.isArray(originalChange)) { - for (let i = 0, l = originalChange.length; i < l; i++) { - originalChange[i](...args); - } - } else if (originalChange) { - originalChange(...args); - } - this.onFieldChange(); - }, - }); - } - return this.renderFormItem([firstChildren, children.slice(1)]); + }; }, + // data() { + // warning(!hasProp(this, 'prop'), `\`prop\` is deprecated. Please use \`name\` instead.`); + // return { + // validateState: this.validateStatus, + // validateMessage: '', + // validateDisabled: false, + // validator: {}, + // helpShow: false, + // errors: [], + // initialValue: undefined, + // }; + // }, + // render() { + // const { autoLink } = getOptionProps(this); + // const children = getSlot(this); + // let firstChildren = children[0]; + // if (this.fieldName && autoLink && isValidElement(firstChildren)) { + // const originalEvents = getEvents(firstChildren); + // const originalBlur = originalEvents.onBlur; + // const originalChange = originalEvents.onChange; + // firstChildren = cloneElement(firstChildren, { + // ...(this.fieldId ? { id: this.fieldId } : undefined), + // onBlur: (...args: any[]) => { + // originalBlur && originalBlur(...args); + // this.onFieldBlur(); + // }, + // onChange: (...args: any[]) => { + // if (Array.isArray(originalChange)) { + // for (let i = 0, l = originalChange.length; i < l; i++) { + // originalChange[i](...args); + // } + // } else if (originalChange) { + // originalChange(...args); + // } + // this.onFieldChange(); + // }, + // }); + // } + // return this.renderFormItem([firstChildren, children.slice(1)]); + // }, }); diff --git a/components/form/FormItemInput.tsx b/components/form/FormItemInput.tsx index 7382421c5..0e0c54d17 100644 --- a/components/form/FormItemInput.tsx +++ b/components/form/FormItemInput.tsx @@ -3,7 +3,7 @@ import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled'; -import Col, { ColProps } from '../grid/col'; +import Col, { ColProps } from '../grid/Col'; import { useProvideForm, useInjectForm, useProvideFormItemPrefix } from './context'; import ErrorList from './ErrorList'; import classNames from '../_util/classNames'; @@ -11,7 +11,7 @@ import { ValidateStatus } from './FormItem'; import { VueNode } from '../_util/type'; import { computed, defineComponent, HTMLAttributes, onUnmounted } from 'vue'; -interface FormItemInputMiscProps { +export interface FormItemInputMiscProps { prefixCls: string; errors: VueNode[]; hasFeedback?: boolean; @@ -32,8 +32,20 @@ const iconMap: { [key: string]: any } = { error: CloseCircleFilled, validating: LoadingOutlined, }; -const FormItemInput = defineComponent({ +const FormItemInput = defineComponent({ slots: ['help', 'extra', 'errors'], + inheritAttrs: false, + props: [ + 'prefixCls', + 'errors', + 'hasFeedback', + 'validateStatus', + 'onDomErrorVisibleChange', + 'wrapperCol', + 'help', + 'extra', + 'status', + ], setup(props, { slots }) { const formContext = useInjectForm(); const { wrapperCol: contextWrapperCol } = formContext; @@ -43,12 +55,15 @@ const FormItemInput = defineComponent props.prefixCls), status: computed(() => props.status), }); + onUnmounted(() => { + props.onDomErrorVisibleChange(false); + }); + return () => { const { prefixCls, @@ -67,10 +82,6 @@ const FormItemInput = defineComponent { - onDomErrorVisibleChange(false); - }); - // Should provides additional icon if `hasFeedback` const IconNode = validateStatus && iconMap[validateStatus]; const icon = diff --git a/components/form/FormItemLabel.tsx b/components/form/FormItemLabel.tsx index 57624bac2..229bb3429 100644 --- a/components/form/FormItemLabel.tsx +++ b/components/form/FormItemLabel.tsx @@ -1,4 +1,4 @@ -import Col, { ColProps } from '../grid/col'; +import Col, { ColProps } from '../grid/Col'; import { FormLabelAlign } from './interface'; import { useInjectForm } from './context'; import { RequiredMark } from './Form'; @@ -17,10 +17,14 @@ export interface FormItemLabelProps { requiredMark?: RequiredMark; required?: boolean; prefixCls: string; + onClick: Function; } -const FormItemLabel: FunctionalComponent = (props, { slots }) => { - const { prefixCls, htmlFor, labelCol, labelAlign, colon, required, requiredMark } = props; +const FormItemLabel: FunctionalComponent = (props, { slots, emit, attrs }) => { + const { prefixCls, htmlFor, labelCol, labelAlign, colon, required, requiredMark } = { + ...props, + ...attrs, + }; const [formLocale] = useLocaleReceiver('Form'); const label = props.label ?? slots.label?.(); if (!label) return null; @@ -68,7 +72,6 @@ const FormItemLabel: FunctionalComponent = (props, { slots } ); } - const labelClassName = classNames({ [`${prefixCls}-item-required`]: required, [`${prefixCls}-item-required-mark-optional`]: requiredMark === 'optional', @@ -80,6 +83,7 @@ const FormItemLabel: FunctionalComponent = (props, { slots } html-for={htmlFor} class={labelClassName} title={typeof label === 'string' ? label : ''} + onClick={e => emit('click', e)} > {labelChildren} @@ -88,5 +92,6 @@ const FormItemLabel: FunctionalComponent = (props, { slots } }; FormItemLabel.displayName = 'FormItemLabel'; +FormItemLabel.inheritAttrs = false; export default FormItemLabel; diff --git a/components/form/context.ts b/components/form/context.ts index 68005e317..00f95342e 100644 --- a/components/form/context.ts +++ b/components/form/context.ts @@ -1,10 +1,11 @@ import { inject, InjectionKey, provide, ComputedRef, computed } from 'vue'; import { ColProps } from '../grid'; -import { RequiredMark } from './Form'; -import { ValidateStatus } from './FormItem'; +import { RequiredMark, ValidationRule } from './Form'; +import { ValidateStatus, FieldExpose } from './FormItem'; import { FormLabelAlign } from './interface'; export interface FormContextProps { + model?: ComputedRef; vertical: ComputedRef; name?: ComputedRef; colon?: ComputedRef; @@ -13,6 +14,10 @@ export interface FormContextProps { wrapperCol?: ComputedRef; requiredMark?: ComputedRef; //itemRef: (name: (string | number)[]) => (node: React.ReactElement) => void; + addField: (eventKey: string, field: FieldExpose) => void; + removeField: (eventKey: string) => void; + validateTrigger?: ComputedRef; + rules?: ComputedRef<{ [k: string]: ValidationRule[] | ValidationRule }>; } export const FormContextKey: InjectionKey = Symbol('formContextKey'); @@ -25,6 +30,10 @@ export const useInjectForm = () => { return inject(FormContextKey, { labelAlign: computed(() => 'right' as FormLabelAlign), vertical: computed(() => false), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + addField: (_eventKey: string, _field: FieldExpose) => {}, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + removeField: (_eventKey: string) => {}, }); }; diff --git a/components/grid/Col.tsx b/components/grid/Col.tsx index 35ee71d82..1875b9e0f 100644 --- a/components/grid/Col.tsx +++ b/components/grid/Col.tsx @@ -1,7 +1,6 @@ -import { inject, defineComponent, CSSProperties, ExtractPropTypes, computed } from 'vue'; +import { defineComponent, CSSProperties, ExtractPropTypes, computed } from 'vue'; import classNames from '../_util/classNames'; import PropTypes from '../_util/vue-types'; -import { rowContextState } from './Row'; import useConfigInject from '../_util/hooks/useConfigInject'; import { useInjectRow } from './context'; @@ -102,7 +101,7 @@ export default defineComponent({ const mergedStyle = computed(() => { const { flex } = props; const gutterVal = gutter.value; - let style: CSSProperties = {}; + const style: CSSProperties = {}; // Horizontal gutter use padding if (gutterVal && gutterVal[0] > 0) { const horizontalGutter = `${gutterVal[0] / 2}px`; diff --git a/components/layout/Sider.tsx b/components/layout/Sider.tsx index 9b7706293..80f1f121e 100644 --- a/components/layout/Sider.tsx +++ b/components/layout/Sider.tsx @@ -211,7 +211,7 @@ export default defineComponent({ return ( ); }; diff --git a/components/skeleton/Image.tsx b/components/skeleton/Image.tsx index aaba6523d..d19c747ef 100644 --- a/components/skeleton/Image.tsx +++ b/components/skeleton/Image.tsx @@ -3,8 +3,7 @@ import classNames from '../_util/classNames'; import useConfigInject from '../_util/hooks/useConfigInject'; import { skeletonElementProps, SkeletonElementProps } from './Element'; -export interface SkeletonImageProps - extends Omit {} +export type SkeletonImageProps = Omit; const path = 'M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z'; diff --git a/components/skeleton/Paragraph.tsx b/components/skeleton/Paragraph.tsx index c809d7a64..52b614cc9 100644 --- a/components/skeleton/Paragraph.tsx +++ b/components/skeleton/Paragraph.tsx @@ -12,8 +12,8 @@ export const skeletonParagraphProps = { export type SkeletonParagraphProps = Partial>; const SkeletonParagraph = defineComponent({ - props: skeletonParagraphProps, name: 'SkeletonParagraph', + props: skeletonParagraphProps, setup(props) { const getWidth = (index: number) => { const { width, rows = 2 } = props; diff --git a/components/skeleton/Skeleton.tsx b/components/skeleton/Skeleton.tsx index f9cc307c8..bb21c6300 100644 --- a/components/skeleton/Skeleton.tsx +++ b/components/skeleton/Skeleton.tsx @@ -10,7 +10,7 @@ import useConfigInject from '../_util/hooks/useConfigInject'; import Element from './Element'; /* This only for skeleton internal. */ -interface SkeletonAvatarProps extends Omit {} +type SkeletonAvatarProps = Omit; export const skeletonProps = { active: PropTypes.looseBool, diff --git a/components/skeleton/Title.tsx b/components/skeleton/Title.tsx index 15c90f3db..be08b2c0e 100644 --- a/components/skeleton/Title.tsx +++ b/components/skeleton/Title.tsx @@ -9,8 +9,8 @@ export const skeletonTitleProps = { export type SkeletonTitleProps = Partial>; const SkeletonTitle = defineComponent({ - props: skeletonTitleProps, name: 'SkeletonTitle', + props: skeletonTitleProps, setup(props) { return () => { const { prefixCls, width } = props;