diff --git a/components/form-model/Form.jsx b/components/form-model/Form.jsx deleted file mode 100755 index 111b19db6..000000000 --- a/components/form-model/Form.jsx +++ /dev/null @@ -1,282 +0,0 @@ -import { inject, provide } from 'vue'; -import PropTypes from '../_util/vue-types'; -import classNames from 'classnames'; -import { ColProps } from '../grid/Col'; -import isRegExp from 'lodash/isRegExp'; -import warning from '../_util/warning'; -import FormItem from './FormItem'; -import { initDefaultProps, getSlot } from '../_util/props-util'; -import { ConfigConsumerProps } from '../config-provider'; -import { getNamePath, containsNamePath } from './utils/valueUtil'; -import { defaultValidateMessages } from './utils/messages'; -import { allPromiseFinish } from './utils/asyncUtil'; -import { toArray } from './utils/typeUtil'; -import isEqual from 'lodash/isEqual'; -import scrollIntoView from 'scroll-into-view-if-needed'; - -export const FormProps = { - layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), - labelCol: PropTypes.shape(ColProps).loose, - wrapperCol: PropTypes.shape(ColProps).loose, - colon: PropTypes.bool, - labelAlign: PropTypes.oneOf(['left', 'right']), - prefixCls: PropTypes.string, - hideRequiredMark: PropTypes.bool, - model: PropTypes.object, - rules: PropTypes.object, - validateMessages: PropTypes.any, - validateOnRuleChange: PropTypes.bool, - // 提交失败自动滚动到第一个错误字段 - scrollToFirstError: PropTypes.bool, - onFinish: PropTypes.func, - onFinishFailed: PropTypes.func, - name: PropTypes.name, -}; - -export const ValidationRule = { - /** validation error message */ - message: PropTypes.string, - /** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */ - type: PropTypes.string, - /** indicates whether field is required */ - required: PropTypes.boolean, - /** treat required fields that only contain whitespace as errors */ - whitespace: PropTypes.boolean, - /** validate the exact length of a field */ - len: PropTypes.number, - /** validate the min length of a field */ - min: PropTypes.number, - /** validate the max length of a field */ - max: PropTypes.number, - /** validate the value from a list of possible values */ - enum: PropTypes.oneOfType([String, PropTypes.arrayOf(String)]), - /** validate from a regular expression */ - pattern: PropTypes.custom(isRegExp), - /** transform a value before validation */ - transform: PropTypes.func, - /** custom validate function (Note: callback must be called) */ - validator: PropTypes.func, -}; - -function isEqualName(name1, name2) { - return isEqual(toArray(name1), toArray(name2)); -} - -const Form = { - name: 'AFormModel', - inheritAttrs: false, - props: initDefaultProps(FormProps, { - layout: 'horizontal', - hideRequiredMark: false, - colon: true, - }), - Item: FormItem, - created() { - this.fields = []; - this.form = undefined; - this.lastValidatePromise = null; - provide('FormContext', this); - }, - setup() { - return { - configProvider: inject('configProvider', ConfigConsumerProps), - }; - }, - watch: { - rules() { - if (this.validateOnRuleChange) { - this.validateFields(); - } - }, - }, - computed: { - vertical() { - return this.layout === 'vertical'; - }, - }, - methods: { - addField(field) { - if (field) { - this.fields.push(field); - } - }, - removeField(field) { - if (field.fieldName) { - this.fields.splice(this.fields.indexOf(field), 1); - } - }, - handleSubmit(e) { - 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) { - const provideNameList = !!nameList; - const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : []; - if (!provideNameList) { - return this.fields; - } else { - return this.fields.filter( - field => namePathList.findIndex(namePath => isEqualName(namePath, field.fieldName)) > -1, - ); - } - }, - resetFields(name) { - if (!this.model) { - warning(false, 'Form', 'model is required for resetFields to work.'); - return; - } - this.getFieldsByNameList(name).forEach(field => { - field.resetField(); - }); - }, - clearValidate(name) { - this.getFieldsByNameList(name).forEach(field => { - field.clearValidate(); - }); - }, - handleFinishFailed(errorInfo) { - const { scrollToFirstError } = this; - this.$emit('finishFailed', errorInfo); - if (scrollToFirstError && errorInfo.errorFields.length) { - this.scrollToField(errorInfo.errorFields[0].name); - } - }, - validate() { - return this.validateField(...arguments); - }, - scrollToField(name, options = {}) { - const fields = this.getFieldsByNameList([name]); - if (fields.length) { - const fieldId = fields[0].fieldId; - const node = fieldId ? document.getElementById(fieldId) : null; - - if (node) { - scrollIntoView(node, { - scrollMode: 'if-needed', - block: 'nearest', - ...options, - }); - } - } - }, - // eslint-disable-next-line no-unused-vars - getFieldsValue(nameList = true) { - const values = {}; - this.fields.forEach(({ fieldName, fieldValue }) => { - values[fieldName] = fieldValue; - }); - if (nameList === true) { - return values; - } else { - const res = {}; - toArray(nameList).forEach(namePath => (res[namePath] = values[namePath])); - return res; - } - }, - validateFields(nameList, options) { - if (!this.model) { - warning(false, 'Form', 'model is required for validateFields to work.'); - return; - } - const provideNameList = !!nameList; - const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : []; - - // Collect result in promise list - const promiseList = []; - - this.fields.forEach(field => { - // Add field if not provide `nameList` - if (!provideNameList) { - namePathList.push(field.getNamePath()); - } - - // Skip if without rule - if (!field.getRules().length) { - return; - } - - const fieldNamePath = field.getNamePath(); - - // Add field validate rule in to promise list - if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) { - const promise = field.validateRules({ - validateMessages: { - ...defaultValidateMessages, - ...this.validateMessages, - }, - ...options, - }); - - // Wrap promise with field - promiseList.push( - promise - .then(() => ({ name: fieldNamePath, errors: [] })) - .catch(errors => - Promise.reject({ - name: fieldNamePath, - errors, - }), - ), - ); - } - }); - - const summaryPromise = allPromiseFinish(promiseList); - this.lastValidatePromise = summaryPromise; - - const returnPromise = summaryPromise - .then(() => { - if (this.lastValidatePromise === summaryPromise) { - return Promise.resolve(this.getFieldsValue(namePathList)); - } - return Promise.reject([]); - }) - .catch(results => { - const errorList = results.filter(result => result && result.errors.length); - return Promise.reject({ - values: this.getFieldsValue(namePathList), - errorFields: errorList, - outOfDate: this.lastValidatePromise !== summaryPromise, - }); - }); - - // Do not throw in console - returnPromise.catch(e => e); - - return returnPromise; - }, - validateField() { - return this.validateFields(...arguments); - }, - }, - - render() { - const { prefixCls: customizePrefixCls, hideRequiredMark, layout, handleSubmit } = this; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('form', customizePrefixCls); - const { class: className, onSubmit: originSubmit, ...restProps } = this.$attrs; - - const formClassName = classNames(prefixCls, className, { - [`${prefixCls}-horizontal`]: layout === 'horizontal', - [`${prefixCls}-vertical`]: layout === 'vertical', - [`${prefixCls}-inline`]: layout === 'inline', - [`${prefixCls}-hide-required-mark`]: hideRequiredMark, - }); - return ( -
- {getSlot(this)} -
- ); - }, -}; - -export default Form; diff --git a/components/form-model/FormItem.jsx b/components/form-model/FormItem.jsx deleted file mode 100644 index d3e21f8b2..000000000 --- a/components/form-model/FormItem.jsx +++ /dev/null @@ -1,498 +0,0 @@ -import { inject, provide, Transition } from 'vue'; -import cloneDeep from 'lodash/cloneDeep'; -import PropTypes from '../_util/vue-types'; -import classNames from 'classnames'; -import getTransitionProps from '../_util/getTransitionProps'; -import Row from '../grid/Row'; -import Col, { ColProps } from '../grid/Col'; -import hasProp, { - initDefaultProps, - findDOMNode, - getComponent, - getOptionProps, - getEvents, - isValidElement, - getSlot, -} from '../_util/props-util'; -import BaseMixin from '../_util/BaseMixin'; -import { ConfigConsumerProps } 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 { getNamePath } from './utils/valueUtil'; -import { toArray } from './utils/typeUtil'; -import { warning } from '../vc-util/warning'; - -const iconMap = { - success: CheckCircleFilled, - warning: ExclamationCircleFilled, - error: CloseCircleFilled, - validating: LoadingOutlined, -}; - -function getPropByPath(obj, namePathList, strict) { - let tempObj = obj; - - const keyArr = namePathList; - let i = 0; - try { - for (let len = keyArr.length; i < len - 1; ++i) { - if (!tempObj && !strict) break; - let key = keyArr[i]; - if (key in tempObj) { - tempObj = tempObj[key]; - } else { - if (strict) { - throw Error('please transfer a valid name path to form item!'); - } - break; - } - } - if (strict && !tempObj) { - throw Error('please transfer a valid name path to form item!'); - } - } catch (error) { - console.error('please transfer a valid name path to form item!'); - } - - return { - o: tempObj, - k: keyArr[i], - v: tempObj ? tempObj[keyArr[i]] : undefined, - }; -} -export const FormItemProps = { - id: PropTypes.string, - htmlFor: PropTypes.string, - prefixCls: PropTypes.string, - label: PropTypes.any, - help: PropTypes.any, - extra: PropTypes.any, - labelCol: PropTypes.shape(ColProps).loose, - wrapperCol: PropTypes.shape(ColProps).loose, - hasFeedback: PropTypes.bool, - colon: PropTypes.bool, - labelAlign: PropTypes.oneOf(['left', 'right']), - prop: PropTypes.oneOfType([Array, String, Number]), - name: PropTypes.oneOfType([Array, String, Number]), - rules: PropTypes.oneOfType([Array, Object]), - autoLink: PropTypes.bool, - required: PropTypes.bool, - validateFirst: PropTypes.bool, - validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']), -}; - -export default { - name: 'AFormModelItem', - mixins: [BaseMixin], - inheritAttrs: false, - __ANT_NEW_FORM_ITEM: true, - props: initDefaultProps(FormItemProps, { - hasFeedback: false, - autoLink: true, - }), - setup() { - return { - isFormItemChildren: inject('isFormItemChildren', false), - configProvider: inject('configProvider', ConfigConsumerProps), - FormContext: inject('FormContext', {}), - }; - }, - data() { - warning(hasProp(this, 'prop'), `\`prop\` is deprecated. Please use \`name\` instead.`); - return { - validateState: this.validateStatus, - validateMessage: '', - validateDisabled: false, - validator: {}, - helpShow: false, - errors: [], - }; - }, - - computed: { - fieldName() { - return this.name || this.prop; - }, - namePath() { - return getNamePath(this.fieldName); - }, - fieldId() { - if (this.id) { - return this.id; - } else if (!this.namePath.length) { - return undefined; - } else { - const formName = this.FormContext.name; - const mergedId = this.namePath.join('_'); - return formName ? `${formName}_${mergedId}` : mergedId; - } - }, - fieldValue() { - const model = this.FormContext.model; - if (!model || !this.fieldName) { - return; - } - return getPropByPath(model, this.namePath, true).v; - }, - isRequired() { - let rules = this.getRules(); - let isRequired = false; - if (rules && rules.length) { - rules.every(rule => { - if (rule.required) { - isRequired = true; - return false; - } - return true; - }); - } - return isRequired || this.required; - }, - }, - 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) { - const { validateFirst = false, messageVariables } = this.$props; - const { triggerName } = options || {}; - const namePath = this.getNamePath(); - - let filteredRules = this.getRules(); - if (triggerName) { - filteredRules = filteredRules.filter(rule => { - const { trigger } = rule; - if (!trigger) { - return true; - } - const triggerList = toArray(trigger); - return triggerList.includes(triggerName); - }); - } - if (!filteredRules.length) { - return Promise.resolve(); - } - const promise = validateRules( - namePath, - this.fieldValue, - filteredRules, - options, - validateFirst, - messageVariables, - ); - this.validateState = 'validating'; - this.errors = []; - - promise - .catch(e => e) - .then((errors = []) => { - if (this.validateState === 'validating') { - this.validateState = errors.length ? 'error' : 'success'; - this.validateMessage = errors[0]; - this.errors = errors; - } - }); - - return promise; - }, - getRules() { - let formRules = this.FormContext.rules; - const selfRules = this.rules; - const requiredRule = - this.required !== undefined ? { required: !!this.required, trigger: 'change' } : []; - const prop = getPropByPath(formRules, this.namePath); - formRules = formRules ? prop.o[prop.k] || prop.v : []; - return [].concat(selfRules || formRules || []).concat(requiredRule); - }, - getFilteredRule(trigger) { - const rules = this.getRules(); - return rules - .filter(rule => { - if (!rule.trigger || trigger === '') return true; - if (Array.isArray(rule.trigger)) { - return rule.trigger.indexOf(trigger) > -1; - } else { - return rule.trigger === trigger; - } - }) - .map(rule => ({ ...rule })); - }, - onFieldBlur() { - this.validateRules({ triggerName: 'blur' }); - }, - onFieldChange() { - if (this.validateDisabled) { - this.validateDisabled = 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; - if (Array.isArray(value)) { - prop.o[prop.k] = [].concat(this.initialValue); - } else { - prop.o[prop.k] = this.initialValue; - } - // reset validateDisabled after onFieldChange triggered - this.$nextTick(() => { - this.validateDisabled = false; - }); - }, - getHelpMessage() { - const help = getComponent(this, 'help'); - - return this.validateMessage || help; - }, - - onLabelClick() { - const id = this.fieldId; - if (!id) { - return; - } - const formItemNode = findDOMNode(this); - const control = formItemNode.querySelector(`[id="${id}"]`); - if (control && control.focus) { - control.focus(); - } - }, - - onHelpAnimEnd(_key, helpShow) { - this.helpShow = helpShow; - if (!helpShow) { - this.$forceUpdate(); - } - }, - - renderHelp(prefixCls) { - 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) { - const extra = getComponent(this, 'extra'); - return extra ?
{extra}
: null; - }, - - renderValidateWrapper(prefixCls, c1, c2, c3) { - const validateStatus = this.validateState; - - let classes = `${prefixCls}-item-control`; - if (validateStatus) { - classes = classNames(`${prefixCls}-item-control`, { - 'has-feedback': this.hasFeedback || validateStatus === 'validating', - 'has-success': validateStatus === 'success', - 'has-warning': validateStatus === 'warning', - 'has-error': validateStatus === 'error', - 'is-validating': validateStatus === 'validating', - }); - } - const IconNode = validateStatus && iconMap[validateStatus]; - - const icon = - this.hasFeedback && IconNode ? ( - - - - ) : null; - return ( -
- - {c1} - {icon} - - {c2} - {c3} -
- ); - }, - - renderWrapper(prefixCls, children) { - const { wrapperCol: contextWrapperCol } = this.isFormItemChildren ? {} : this.FormContext; - const { wrapperCol } = this; - const mergedWrapperCol = wrapperCol || contextWrapperCol || {}; - const { style, id, ...restProps } = mergedWrapperCol; - const className = classNames(`${prefixCls}-item-control-wrapper`, mergedWrapperCol.class); - const colProps = { - ...restProps, - class: className, - key: 'wrapper', - style, - id, - }; - return {children}; - }, - - renderLabel(prefixCls) { - 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, child) { - return [ - this.renderLabel(prefixCls), - this.renderWrapper( - prefixCls, - this.renderValidateWrapper( - prefixCls, - child, - this.renderHelp(prefixCls), - this.renderExtra(prefixCls), - ), - ), - ]; - }, - renderFormItem(child) { - const { prefixCls: customizePrefixCls } = this.$props; - const { class: className, ...restProps } = this.$attrs; - 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, - }; - - return ( - - {children} - - ); - }, - }, - 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) => { - originalBlur && originalBlur(...args); - this.onFieldBlur(); - }, - onChange: (...args) => { - 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-model/__tests__/__snapshots__/demo.test.js.snap b/components/form-model/__tests__/__snapshots__/demo.test.js.snap deleted file mode 100644 index 5a248ee90..000000000 --- a/components/form-model/__tests__/__snapshots__/demo.test.js.snap +++ /dev/null @@ -1,282 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders ./antdv-demo/docs/form-model/demo/basic.md correctly 1`] = ` -
-
-
-
-
- -
-
-
-
-
-
-
please select your zone
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-`; - -exports[`renders ./antdv-demo/docs/form-model/demo/custom-validation.md correctly 1`] = ` -
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-`; - -exports[`renders ./antdv-demo/docs/form-model/demo/dynamic-form-item.md correctly 1`] = ` -
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-`; - -exports[`renders ./antdv-demo/docs/form-model/demo/horizontal-login.md correctly 1`] = ` -
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-`; - -exports[`renders ./antdv-demo/docs/form-model/demo/layout.md correctly 1`] = ` -
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-`; - -exports[`renders ./antdv-demo/docs/form-model/demo/validation.md correctly 1`] = ` -
-
-
-
-
- -
-
-
-
-
-
-
please select your zone
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-`; diff --git a/components/form-model/__tests__/demo.test.js b/components/form-model/__tests__/demo.test.js deleted file mode 100644 index 22a74f8a8..000000000 --- a/components/form-model/__tests__/demo.test.js +++ /dev/null @@ -1,3 +0,0 @@ -import demoTest from '../../../tests/shared/demoTest'; - -demoTest('form-model'); diff --git a/components/form-model/index.jsx b/components/form-model/index.jsx deleted file mode 100644 index b63f33cf5..000000000 --- a/components/form-model/index.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import Form from './Form'; - -export { FormProps, ValidationRule } from './Form'; -export { FormItemProps } from './FormItem'; - -/* istanbul ignore next */ -Form.install = function(app) { - app.component(Form.name, Form); - app.component(Form.Item.name, Form.Item); -}; - -export default Form; diff --git a/components/form-old/Form.jsx b/components/form-old/Form.jsx new file mode 100755 index 000000000..22dd38637 --- /dev/null +++ b/components/form-old/Form.jsx @@ -0,0 +1,287 @@ +import PropTypes from '../_util/vue-types'; +import classNames from 'classnames'; +import { ColProps } from '../grid/Col'; +import Vue from 'vue'; +import isRegExp from 'lodash/isRegExp'; +import warning from '../_util/warning'; +import createDOMForm from '../vc-form/src/createDOMForm'; +import createFormField from '../vc-form/src/createFormField'; +import FormItem from './FormItem'; +import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; +import { initDefaultProps, getListeners } from '../_util/props-util'; +import { ConfigConsumerProps } from '../config-provider'; + +export const FormCreateOption = { + onFieldsChange: PropTypes.func, + onValuesChange: PropTypes.func, + mapPropsToFields: PropTypes.func, + validateMessages: PropTypes.any, + withRef: PropTypes.bool, + name: PropTypes.string, +}; + +// function create +export const WrappedFormUtils = { + /** 获取一组输入控件的值,如不传入参数,则获取全部组件的值 */ + getFieldsValue: PropTypes.func, + /** 获取一个输入控件的值*/ + getFieldValue: PropTypes.func, + /** 设置一组输入控件的值*/ + setFieldsValue: PropTypes.func, + /** 设置一组输入控件的值*/ + setFields: PropTypes.func, + /** 校验并获取一组输入域的值与 Error */ + validateFields: PropTypes.func, + // validateFields(fieldNames: Array, options: Object, callback: ValidateCallback): void; + // validateFields(fieldNames: Array, callback: ValidateCallback): void; + // validateFields(options: Object, callback: ValidateCallback): void; + // validateFields(callback: ValidateCallback): void; + // validateFields(): void; + /** 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 */ + validateFieldsAndScroll: PropTypes.func, + // validateFieldsAndScroll(fieldNames?: Array, options?: Object, callback?: ValidateCallback): void; + // validateFieldsAndScroll(fieldNames?: Array, callback?: ValidateCallback): void; + // validateFieldsAndScroll(options?: Object, callback?: ValidateCallback): void; + // validateFieldsAndScroll(callback?: ValidateCallback): void; + // validateFieldsAndScroll(): void; + /** 获取某个输入控件的 Error */ + getFieldError: PropTypes.func, + getFieldsError: PropTypes.func, + /** 判断一个输入控件是否在校验状态*/ + isFieldValidating: PropTypes.func, + isFieldTouched: PropTypes.func, + isFieldsTouched: PropTypes.func, + /** 重置一组输入控件的值与状态,如不传入参数,则重置所有组件 */ + resetFields: PropTypes.func, + + getFieldDecorator: PropTypes.func, +}; + +export const FormProps = { + layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), + labelCol: PropTypes.shape(ColProps).loose, + wrapperCol: PropTypes.shape(ColProps).loose, + colon: PropTypes.bool, + labelAlign: PropTypes.oneOf(['left', 'right']), + form: PropTypes.object, + // onSubmit: React.FormEventHandler; + prefixCls: PropTypes.string, + hideRequiredMark: PropTypes.bool, + autoFormCreate: PropTypes.func, + options: PropTypes.object, + selfUpdate: PropTypes.bool, +}; + +export const ValidationRule = { + /** validation error message */ + message: PropTypes.string, + /** built-in validation type, available options: https://github.com/yiminghe/async-validator#type */ + type: PropTypes.string, + /** indicates whether field is required */ + required: PropTypes.boolean, + /** treat required fields that only contain whitespace as errors */ + whitespace: PropTypes.boolean, + /** validate the exact length of a field */ + len: PropTypes.number, + /** validate the min length of a field */ + min: PropTypes.number, + /** validate the max length of a field */ + max: PropTypes.number, + /** validate the value from a list of possible values */ + enum: PropTypes.oneOfType([String, PropTypes.arrayOf(String)]), + /** validate from a regular expression */ + pattern: PropTypes.custom(isRegExp), + /** transform a value before validation */ + transform: PropTypes.func, + /** custom validate function (Note: callback must be called) */ + validator: PropTypes.func, +}; + +// export type ValidateCallback = (errors: any, values: any) => void; + +// export type GetFieldDecoratorOptions = { +// /** 子节点的值的属性,如 Checkbox 的是 'checked' */ +// valuePropName?: string; +// /** 子节点的初始值,类型、可选值均由子节点决定 */ +// initialValue?: any; +// /** 收集子节点的值的时机 */ +// trigger?: string; +// /** 可以把 onChange 的参数转化为控件的值,例如 DatePicker 可设为:(date, dateString) => dateString */ +// getValueFromEvent?: (...args: any[]) => any; +// /** Get the component props according to field value. */ +// getValueProps?: (value: any) => any; +// /** 校验子节点值的时机 */ +// validateTrigger?: string | string[]; +// /** 校验规则,参见 [async-validator](https://github.com/yiminghe/async-validator) */ +// rules?: ValidationRule[]; +// /** 是否和其他控件互斥,特别用于 Radio 单选控件 */ +// exclusive?: boolean; +// /** Normalize value to form component */ +// normalize?: (value: any, prevValue: any, allValues: any) => any; +// /** Whether stop validate on first rule of error for this field. */ +// validateFirst?: boolean; +// /** 是否一直保留子节点的信息 */ +// preserve?: boolean; +// }; + +const Form = { + name: 'AForm', + props: initDefaultProps(FormProps, { + layout: 'horizontal', + hideRequiredMark: false, + colon: true, + }), + Item: FormItem, + createFormField, + create: (options = {}) => { + return createDOMForm({ + fieldNameProp: 'id', + ...options, + fieldMetaProp: FIELD_META_PROP, + fieldDataProp: FIELD_DATA_PROP, + }); + }, + createForm(context, options = {}) { + const V = Vue; + return new V(Form.create({ ...options, templateContext: context })()); + }, + created() { + this.formItemContexts = new Map(); + }, + provide() { + return { + FormContext: this, + // https://github.com/vueComponent/ant-design-vue/issues/446 + collectFormItemContext: + this.form && this.form.templateContext + ? (c, type = 'add') => { + const formItemContexts = this.formItemContexts; + const number = formItemContexts.get(c) || 0; + if (type === 'delete') { + if (number <= 1) { + formItemContexts.delete(c); + } else { + formItemContexts.set(c, number - 1); + } + } else { + if (c !== this.form.templateContext) { + formItemContexts.set(c, number + 1); + } + } + } + : () => {}, + }; + }, + inject: { + configProvider: { default: () => ConfigConsumerProps }, + }, + watch: { + form() { + this.$forceUpdate(); + }, + }, + computed: { + vertical() { + return this.layout === 'vertical'; + }, + }, + beforeUpdate() { + this.formItemContexts.forEach((number, c) => { + if (c.$forceUpdate) { + c.$forceUpdate(); + } + }); + }, + updated() { + if (this.form && this.form.cleanUpUselessFields) { + this.form.cleanUpUselessFields(); + } + }, + methods: { + onSubmit(e) { + if (!getListeners(this).submit) { + e.preventDefault(); + } else { + this.$emit('submit', e); + } + }, + }, + + render() { + const { + prefixCls: customizePrefixCls, + hideRequiredMark, + layout, + onSubmit, + $slots, + autoFormCreate, + options = {}, + } = this; + const getPrefixCls = this.configProvider.getPrefixCls; + const prefixCls = getPrefixCls('form', customizePrefixCls); + + const formClassName = classNames(prefixCls, { + [`${prefixCls}-horizontal`]: layout === 'horizontal', + [`${prefixCls}-vertical`]: layout === 'vertical', + [`${prefixCls}-inline`]: layout === 'inline', + [`${prefixCls}-hide-required-mark`]: hideRequiredMark, + }); + if (autoFormCreate) { + warning(false, 'Form', '`autoFormCreate` is deprecated. please use `form` instead.'); + const DomForm = + this.DomForm || + createDOMForm({ + fieldNameProp: 'id', + ...options, + fieldMetaProp: FIELD_META_PROP, + fieldDataProp: FIELD_DATA_PROP, + templateContext: this.$vnode.context, + })({ + provide() { + return { + decoratorFormProps: this.$props, + }; + }, + data() { + return { + children: $slots.default, + formClassName, + submit: onSubmit, + }; + }, + created() { + autoFormCreate(this.form); + }, + render() { + const { children, formClassName, submit } = this; + return ( +
+ {children} +
+ ); + }, + }); + if (this.domForm) { + this.domForm.children = $slots.default; + this.domForm.submit = onSubmit; + this.domForm.formClassName = formClassName; + } + this.DomForm = DomForm; + + return ( + { + this.domForm = inst; + }} + /> + ); + } + return ( +
+ {$slots.default} +
+ ); + }, +}; + +export default Form; diff --git a/components/form-old/FormItem.jsx b/components/form-old/FormItem.jsx new file mode 100644 index 000000000..64744fb08 --- /dev/null +++ b/components/form-old/FormItem.jsx @@ -0,0 +1,520 @@ +import { provide, inject, Transition } from 'vue'; +import PropTypes from '../_util/vue-types'; +import classNames from 'classnames'; +import find from 'lodash/find'; +import Row from '../grid/Row'; +import Col, { ColProps } from '../grid/Col'; +import warning from '../_util/warning'; +import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; +import { + initDefaultProps, + getComponent, + getSlotOptions, + isValidElement, + getAllChildren, + findDOMNode, + getSlot, +} from '../_util/props-util'; +import getTransitionProps from '../_util/getTransitionProps'; +import BaseMixin from '../_util/BaseMixin'; +import { cloneElement, cloneVNodes } 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 { ConfigConsumerProps } from '../config-provider'; + +const iconMap = { + success: CheckCircleFilled, + warning: ExclamationCircleFilled, + error: CloseCircleFilled, + validating: LoadingOutlined, +}; + +function noop() {} + +function intersperseSpace(list) { + return list.reduce((current, item) => [...current, ' ', item], []).slice(1); +} +export const FormItemProps = { + id: PropTypes.string, + htmlFor: PropTypes.string, + prefixCls: PropTypes.string, + label: PropTypes.any, + labelCol: PropTypes.shape(ColProps).loose, + wrapperCol: PropTypes.shape(ColProps).loose, + help: PropTypes.any, + extra: PropTypes.any, + validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']), + hasFeedback: PropTypes.bool, + required: PropTypes.bool, + colon: PropTypes.bool, + fieldDecoratorId: PropTypes.string, + fieldDecoratorOptions: PropTypes.object, + selfUpdate: PropTypes.bool, + labelAlign: PropTypes.oneOf(['left', 'right']), +}; +function comeFromSlot(vnodes = [], itemVnode) { + let isSlot = false; + for (let i = 0, len = vnodes.length; i < len; i++) { + const vnode = vnodes[i]; + if (vnode && (vnode === itemVnode || vnode.$vnode === itemVnode)) { + isSlot = true; + } else { + const componentOptions = + vnode.componentOptions || (vnode.$vnode && vnode.$vnode.componentOptions); + const children = componentOptions ? componentOptions.children : vnode.$children; + isSlot = comeFromSlot(children, itemVnode); + } + if (isSlot) { + break; + } + } + return isSlot; +} + +export default { + name: 'AFormItem', + mixins: [BaseMixin], + inheritAttrs: false, + __ANT_FORM_ITEM: true, + props: initDefaultProps(FormItemProps, { + hasFeedback: false, + }), + setup() { + return { + isFormItemChildren: inject('isFormItemChildren', false), + FormContext: inject('FormContext', {}), + decoratorFormProps: inject('decoratorFormProps', {}), + collectFormItemContext: inject('collectFormItemContext', noop), + configProvider: inject('configProvider', ConfigConsumerProps), + }; + }, + data() { + return { helpShow: false }; + }, + computed: { + itemSelfUpdate() { + return !!(this.selfUpdate === undefined ? this.FormContext.selfUpdate : this.selfUpdate); + }, + }, + created() { + provide('isFormItemChildren', true); + this.collectContext(); + }, + beforeUpdate() { + if (process.env.NODE_ENV !== 'production') { + this.collectContext(); + } + }, + beforeUnmount() { + this.collectFormItemContext(this.$vnode && this.$vnode.context, 'delete'); + }, + mounted() { + const { help, validateStatus } = this.$props; + warning( + this.getControls(this.slotDefault, true).length <= 1 || + help !== undefined || + validateStatus !== undefined, + 'Form.Item', + 'Cannot generate `validateStatus` and `help` automatically, ' + + 'while there are more than one `getFieldDecorator` in it.', + ); + warning( + !this.fieldDecoratorId, + 'Form.Item', + '`fieldDecoratorId` is deprecated. please use `v-decorator={id, options}` instead.', + ); + }, + methods: { + collectContext() { + if (this.FormContext.form && this.FormContext.form.templateContext) { + const { templateContext } = this.FormContext.form; + const vnodes = Object.values(templateContext.$slots || {}).reduce((a, b) => { + return [...a, ...b]; + }, []); + const isSlot = comeFromSlot(vnodes, this.$vnode); + warning(!isSlot, 'You can not set FormItem from slot, please use slot-scope instead slot'); + let isSlotScope = false; + // 进一步判断是否是通过slot-scope传递 + if (!isSlot && this.$vnode.context !== templateContext) { + isSlotScope = comeFromSlot(this.$vnode.context.$children, templateContext.$vnode); + } + if (!isSlotScope && !isSlot) { + this.collectFormItemContext(this.$vnode.context); + } + } + }, + getHelpMessage() { + const help = getComponent(this, 'help'); + const onlyControl = this.getOnlyControl(); + if (help === undefined && onlyControl) { + const errors = this.getField().errors; + if (errors) { + return intersperseSpace( + errors.map((e, index) => { + let node = null; + if (isValidElement(e)) { + node = e; + } else if (isValidElement(e.message)) { + node = e.message; + } + return node ? cloneElement(node, { key: index }) : e.message; + }), + ); + } else { + return ''; + } + } + + return help; + }, + + getControls(childrenArray = [], recursively) { + let controls = []; + for (let i = 0; i < childrenArray.length; i++) { + if (!recursively && controls.length > 0) { + break; + } + + const child = childrenArray[i]; + // if (!child.tag && child.text.trim() === '') { + // continue; + // } + + if (typeof child.type === 'object' && child.type.__ANT_FORM_ITEM) { + continue; + } + const children = getAllChildren(child); + const attrs = child.props || {}; + if (FIELD_META_PROP in attrs) { + // And means FIELD_DATA_PROP in child.props, too. + controls.push(child); + } else if (children) { + controls = controls.concat(this.getControls(children, recursively)); + } + } + return controls; + }, + + getOnlyControl() { + const child = this.getControls(this.slotDefault, false)[0]; + return child !== undefined ? child : null; + }, + + getChildAttr(prop) { + const child = this.getOnlyControl(); + let data = {}; + if (!child) { + return undefined; + } + debugger; + if (child.data) { + data = child.data; + } else if (child.$vnode && child.$vnode.data) { + data = child.$vnode.data; + } + return data[prop] || data.attrs[prop]; + }, + + getId() { + return this.getChildAttr('id'); + }, + + getMeta() { + return this.getChildAttr(FIELD_META_PROP); + }, + + getField() { + return this.getChildAttr(FIELD_DATA_PROP); + }, + + getValidateStatus() { + const onlyControl = this.getOnlyControl(); + if (!onlyControl) { + return ''; + } + const field = this.getField(); + if (field.validating) { + return 'validating'; + } + if (field.errors) { + return 'error'; + } + const fieldValue = 'value' in field ? field.value : this.getMeta().initialValue; + if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { + return 'success'; + } + return ''; + }, + + // Resolve duplicated ids bug between different forms + // https://github.com/ant-design/ant-design/issues/7351 + onLabelClick() { + const id = this.id || this.getId(); + if (!id) { + return; + } + const formItemNode = findDOMNode(this); + const control = formItemNode.querySelector(`[id="${id}"]`); + if (control && control.focus) { + control.focus(); + } + }, + + onHelpAnimEnd(_key, helpShow) { + this.helpShow = helpShow; + if (!helpShow) { + this.$forceUpdate(); + } + }, + + isRequired() { + const { required } = this; + if (required !== undefined) { + return required; + } + if (this.getOnlyControl()) { + const meta = this.getMeta() || {}; + const validate = meta.validate || []; + + return validate + .filter(item => !!item.rules) + .some(item => { + return item.rules.some(rule => rule.required); + }); + } + return false; + }, + + renderHelp(prefixCls) { + 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) { + const extra = getComponent(this, 'extra'); + return extra ?
{extra}
: null; + }, + + renderValidateWrapper(prefixCls, c1, c2, c3) { + const props = this.$props; + const onlyControl = this.getOnlyControl; + const validateStatus = + props.validateStatus === undefined && onlyControl + ? this.getValidateStatus() + : props.validateStatus; + + let classes = `${prefixCls}-item-control`; + if (validateStatus) { + classes = classNames(`${prefixCls}-item-control`, { + 'has-feedback': props.hasFeedback || validateStatus === 'validating', + 'has-success': validateStatus === 'success', + 'has-warning': validateStatus === 'warning', + 'has-error': validateStatus === 'error', + 'is-validating': validateStatus === 'validating', + }); + } + const IconNode = validateStatus && iconMap[validateStatus]; + + const icon = + props.hasFeedback && IconNode ? ( + + + + ) : null; + return ( +
+ + {c1} + {icon} + + {c2} + {c3} +
+ ); + }, + + renderWrapper(prefixCls, children) { + const { wrapperCol: contextWrapperCol } = this.isFormItemChildren ? {} : this.FormContext; + const { wrapperCol } = this; + const mergedWrapperCol = wrapperCol || contextWrapperCol || {}; + const { style, id, ...restProps } = mergedWrapperCol; + const className = classNames(`${prefixCls}-item-control-wrapper`, mergedWrapperCol.class); + const colProps = { + ...restProps, + class: className, + key: 'wrapper', + style, + id, + }; + return {children}; + }, + + renderLabel(prefixCls) { + const { + vertical, + labelAlign: contextLabelAlign, + labelCol: contextLabelCol, + colon: contextColon, + } = this.FormContext; + const { labelAlign, labelCol, colon, id, 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) { + return [ + this.renderLabel(prefixCls), + this.renderWrapper( + prefixCls, + this.renderValidateWrapper( + prefixCls, + this.slotDefault, + this.renderHelp(prefixCls), + this.renderExtra(prefixCls), + ), + ), + ]; + }, + renderFormItem() { + const { prefixCls: customizePrefixCls } = this.$props; + const { class: className, ...restProps } = this.$attrs; + const getPrefixCls = this.configProvider.getPrefixCls; + const prefixCls = getPrefixCls('form', customizePrefixCls); + const children = this.renderChildren(prefixCls); + const itemClassName = { + [className]: true, + [`${prefixCls}-item`]: true, + [`${prefixCls}-item-with-help`]: this.helpShow, + }; + + return ( + + {children} + + ); + }, + decoratorOption(vnode) { + if (vnode.data && vnode.data.directives) { + const directive = find(vnode.data.directives, ['name', 'decorator']); + warning( + !directive || (directive && Array.isArray(directive.value)), + 'Form', + `Invalid directive: type check failed for directive "decorator". Expected Array, got ${typeof (directive + ? directive.value + : directive)}. At ${vnode.tag}.`, + ); + return directive ? directive.value : null; + } else { + return null; + } + }, + decoratorChildren(vnodes) { + const { FormContext } = this; + const getFieldDecorator = FormContext.form.getFieldDecorator; + for (let i = 0, len = vnodes.length; i < len; i++) { + const vnode = vnodes[i]; + if (getSlotOptions(vnode).__ANT_FORM_ITEM) { + break; + } + if (vnode.children) { + vnode.children = this.decoratorChildren(cloneVNodes(vnode.children)); + } else if (vnode.componentOptions && vnode.componentOptions.children) { + vnode.componentOptions.children = this.decoratorChildren( + cloneVNodes(vnode.componentOptions.children), + ); + } + const option = this.decoratorOption(vnode); + if (option && option[0]) { + vnodes[i] = getFieldDecorator(option[0], option[1], this)(vnode); + } + } + return vnodes; + }, + }, + + render() { + const { decoratorFormProps, fieldDecoratorId, fieldDecoratorOptions = {}, FormContext } = this; + let child = getSlot(this); + if (decoratorFormProps.form && fieldDecoratorId && child.length) { + const getFieldDecorator = decoratorFormProps.form.getFieldDecorator; + child[0] = getFieldDecorator(fieldDecoratorId, fieldDecoratorOptions, this)(child[0]); + warning( + !(child.length > 1), + 'Form', + '`autoFormCreate` just `decorator` then first children. but you can use JSX to support multiple children', + ); + this.slotDefault = child; + } else if (FormContext.form) { + child = cloneVNodes(child); + this.slotDefault = this.decoratorChildren(child); + } else { + this.slotDefault = child; + } + return this.renderFormItem(); + }, +}; diff --git a/components/form-old/__tests__/__snapshots__/demo.test.js.snap b/components/form-old/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 000000000..97fd53963 --- /dev/null +++ b/components/form-old/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,849 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./antdv-demo/docs/form/demo/advanced-search.vue correctly 1`] = ` + +`; + +exports[`renders ./antdv-demo/docs/form/demo/coordinated.vue correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
Select a option and change input text above
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/customized-form-controls.vue correctly 1`] = ` +
+
+
+
+
RMB
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/dynamic-form-item.vue correctly 1`] = ` +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/dynamic-rule.vue correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/form-in-modal.vue correctly 1`] = ` +
+ +
+`; + +exports[`renders ./antdv-demo/docs/form/demo/global-state.vue correctly 1`] = ` +
+
+
+
+
+
+ +
+
+
+
    {
+  "username": {
+    "value": "benjycui"
+  }
+}
+  
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/horizontal-login.vue correctly 1`] = ` +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/layout.vue correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/normal-login.vue correctly 1`] = ` + +`; + +exports[`renders ./antdv-demo/docs/form/demo/register.vue correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
Zhejiang / Hangzhou / West Lake + +
+
+
+
+
+
+
+ +86 +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
We must make sure that your are a human.
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/time-related-controls.vue correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
~ + +
+
+
+
+
+
+
~ + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/validate-other.vue correctly 1`] = ` +
+
+
+
+
+ China + + +
+
+
+
+
+
+
Please select a country
+
+ +
+
+
+
+
+
+
Please select favourite colors
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ machines +
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
ABCDEF
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
  • +
    +
    +
  • +
  • +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    +
  • +
  • +
    +
    +
    +
    +
  • +
+ +
+
+
+
+
+
+
+ +
longgggggggggggggggggggggggggggggggggg
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/validate-static.vue correctly 1`] = ` +
+
+
+
+
+
Should be combination of numbers & alphabets
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
The information is being validated...
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
Should be combination of numbers & alphabets
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ Option 1 +
+
+ +
+
+
+
+
+
+
+
The information is being validated...
+
+
+
+
+
+
+
+
Please select the correct date
+
+
+
+ - + +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/form/demo/without-form-create.vue correctly 1`] = ` +
+
+
+
+
+
+
+
A prime is a natural number greater than 1 that has no positive divisors other than 1 and itself.
+
+
+
+
+`; diff --git a/components/form/__tests__/__snapshots__/index.test.js.snap b/components/form-old/__tests__/__snapshots__/index.test.js.snap similarity index 100% rename from components/form/__tests__/__snapshots__/index.test.js.snap rename to components/form-old/__tests__/__snapshots__/index.test.js.snap diff --git a/components/form/__tests__/__snapshots__/message.test.js.snap b/components/form-old/__tests__/__snapshots__/message.test.js.snap similarity index 100% rename from components/form/__tests__/__snapshots__/message.test.js.snap rename to components/form-old/__tests__/__snapshots__/message.test.js.snap diff --git a/components/form-old/__tests__/demo.test.js b/components/form-old/__tests__/demo.test.js new file mode 100644 index 000000000..e5338b89c --- /dev/null +++ b/components/form-old/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('form', { suffix: 'vue', skip: ['index.vue', 'vuex.vue'] }); diff --git a/components/form/__tests__/index.test.js b/components/form-old/__tests__/index.test.js similarity index 100% rename from components/form/__tests__/index.test.js rename to components/form-old/__tests__/index.test.js diff --git a/components/form/__tests__/label.test.js b/components/form-old/__tests__/label.test.js similarity index 100% rename from components/form/__tests__/label.test.js rename to components/form-old/__tests__/label.test.js diff --git a/components/form/__tests__/message.test.js b/components/form-old/__tests__/message.test.js similarity index 100% rename from components/form/__tests__/message.test.js rename to components/form-old/__tests__/message.test.js diff --git a/components/form/constants.jsx b/components/form-old/constants.jsx similarity index 100% rename from components/form/constants.jsx rename to components/form-old/constants.jsx diff --git a/components/form-old/index.jsx b/components/form-old/index.jsx new file mode 100644 index 000000000..b5c0b1602 --- /dev/null +++ b/components/form-old/index.jsx @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import Form from './Form'; +import ref from 'vue-ref'; +import FormDecoratorDirective from '../_util/FormDecoratorDirective'; + +Vue.use(ref, { name: 'ant-ref' }); +Vue.use(FormDecoratorDirective); +Vue.prototype.$form = Form; + +export { FormProps, FormCreateOption, ValidationRule } from './Form'; +export { FormItemProps } from './FormItem'; + +/* istanbul ignore next */ +Form.install = function(Vue) { + Vue.component(Form.name, Form); + Vue.component(Form.Item.name, Form.Item); + Vue.prototype.$form = Form; +}; + +export default Form; diff --git a/components/form-model/style/index.js b/components/form-old/style/index.js similarity index 100% rename from components/form-model/style/index.js rename to components/form-old/style/index.js diff --git a/components/form-model/style/index.less b/components/form-old/style/index.less similarity index 100% rename from components/form-model/style/index.less rename to components/form-old/style/index.less diff --git a/components/form-model/style/mixin.less b/components/form-old/style/mixin.less similarity index 100% rename from components/form-model/style/mixin.less rename to components/form-old/style/mixin.less diff --git a/components/form/Form.jsx b/components/form/Form.jsx index 22dd38637..d98a441bd 100755 --- a/components/form/Form.jsx +++ b/components/form/Form.jsx @@ -1,61 +1,18 @@ +import { inject, provide } from 'vue'; import PropTypes from '../_util/vue-types'; import classNames from 'classnames'; import { ColProps } from '../grid/Col'; -import Vue from 'vue'; import isRegExp from 'lodash/isRegExp'; import warning from '../_util/warning'; -import createDOMForm from '../vc-form/src/createDOMForm'; -import createFormField from '../vc-form/src/createFormField'; import FormItem from './FormItem'; -import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; -import { initDefaultProps, getListeners } from '../_util/props-util'; +import { initDefaultProps, getSlot } from '../_util/props-util'; import { ConfigConsumerProps } from '../config-provider'; - -export const FormCreateOption = { - onFieldsChange: PropTypes.func, - onValuesChange: PropTypes.func, - mapPropsToFields: PropTypes.func, - validateMessages: PropTypes.any, - withRef: PropTypes.bool, - name: PropTypes.string, -}; - -// function create -export const WrappedFormUtils = { - /** 获取一组输入控件的值,如不传入参数,则获取全部组件的值 */ - getFieldsValue: PropTypes.func, - /** 获取一个输入控件的值*/ - getFieldValue: PropTypes.func, - /** 设置一组输入控件的值*/ - setFieldsValue: PropTypes.func, - /** 设置一组输入控件的值*/ - setFields: PropTypes.func, - /** 校验并获取一组输入域的值与 Error */ - validateFields: PropTypes.func, - // validateFields(fieldNames: Array, options: Object, callback: ValidateCallback): void; - // validateFields(fieldNames: Array, callback: ValidateCallback): void; - // validateFields(options: Object, callback: ValidateCallback): void; - // validateFields(callback: ValidateCallback): void; - // validateFields(): void; - /** 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 */ - validateFieldsAndScroll: PropTypes.func, - // validateFieldsAndScroll(fieldNames?: Array, options?: Object, callback?: ValidateCallback): void; - // validateFieldsAndScroll(fieldNames?: Array, callback?: ValidateCallback): void; - // validateFieldsAndScroll(options?: Object, callback?: ValidateCallback): void; - // validateFieldsAndScroll(callback?: ValidateCallback): void; - // validateFieldsAndScroll(): void; - /** 获取某个输入控件的 Error */ - getFieldError: PropTypes.func, - getFieldsError: PropTypes.func, - /** 判断一个输入控件是否在校验状态*/ - isFieldValidating: PropTypes.func, - isFieldTouched: PropTypes.func, - isFieldsTouched: PropTypes.func, - /** 重置一组输入控件的值与状态,如不传入参数,则重置所有组件 */ - resetFields: PropTypes.func, - - getFieldDecorator: PropTypes.func, -}; +import { getNamePath, containsNamePath } from './utils/valueUtil'; +import { defaultValidateMessages } from './utils/messages'; +import { allPromiseFinish } from './utils/asyncUtil'; +import { toArray } from './utils/typeUtil'; +import isEqual from 'lodash/isEqual'; +import scrollIntoView from 'scroll-into-view-if-needed'; export const FormProps = { layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), @@ -63,13 +20,17 @@ export const FormProps = { wrapperCol: PropTypes.shape(ColProps).loose, colon: PropTypes.bool, labelAlign: PropTypes.oneOf(['left', 'right']), - form: PropTypes.object, - // onSubmit: React.FormEventHandler; prefixCls: PropTypes.string, hideRequiredMark: PropTypes.bool, - autoFormCreate: PropTypes.func, - options: PropTypes.object, - selfUpdate: PropTypes.bool, + model: PropTypes.object, + rules: PropTypes.object, + validateMessages: PropTypes.any, + validateOnRuleChange: PropTypes.bool, + // 提交失败自动滚动到第一个错误字段 + scrollToFirstError: PropTypes.bool, + onFinish: PropTypes.func, + onFinishFailed: PropTypes.func, + name: PropTypes.name, }; export const ValidationRule = { @@ -97,87 +58,35 @@ export const ValidationRule = { validator: PropTypes.func, }; -// export type ValidateCallback = (errors: any, values: any) => void; - -// export type GetFieldDecoratorOptions = { -// /** 子节点的值的属性,如 Checkbox 的是 'checked' */ -// valuePropName?: string; -// /** 子节点的初始值,类型、可选值均由子节点决定 */ -// initialValue?: any; -// /** 收集子节点的值的时机 */ -// trigger?: string; -// /** 可以把 onChange 的参数转化为控件的值,例如 DatePicker 可设为:(date, dateString) => dateString */ -// getValueFromEvent?: (...args: any[]) => any; -// /** Get the component props according to field value. */ -// getValueProps?: (value: any) => any; -// /** 校验子节点值的时机 */ -// validateTrigger?: string | string[]; -// /** 校验规则,参见 [async-validator](https://github.com/yiminghe/async-validator) */ -// rules?: ValidationRule[]; -// /** 是否和其他控件互斥,特别用于 Radio 单选控件 */ -// exclusive?: boolean; -// /** Normalize value to form component */ -// normalize?: (value: any, prevValue: any, allValues: any) => any; -// /** Whether stop validate on first rule of error for this field. */ -// validateFirst?: boolean; -// /** 是否一直保留子节点的信息 */ -// preserve?: boolean; -// }; +function isEqualName(name1, name2) { + return isEqual(toArray(name1), toArray(name2)); +} const Form = { name: 'AForm', + inheritAttrs: false, props: initDefaultProps(FormProps, { layout: 'horizontal', hideRequiredMark: false, colon: true, }), Item: FormItem, - createFormField, - create: (options = {}) => { - return createDOMForm({ - fieldNameProp: 'id', - ...options, - fieldMetaProp: FIELD_META_PROP, - fieldDataProp: FIELD_DATA_PROP, - }); - }, - createForm(context, options = {}) { - const V = Vue; - return new V(Form.create({ ...options, templateContext: context })()); - }, created() { - this.formItemContexts = new Map(); + this.fields = []; + this.form = undefined; + this.lastValidatePromise = null; + provide('FormContext', this); }, - provide() { + setup() { return { - FormContext: this, - // https://github.com/vueComponent/ant-design-vue/issues/446 - collectFormItemContext: - this.form && this.form.templateContext - ? (c, type = 'add') => { - const formItemContexts = this.formItemContexts; - const number = formItemContexts.get(c) || 0; - if (type === 'delete') { - if (number <= 1) { - formItemContexts.delete(c); - } else { - formItemContexts.set(c, number - 1); - } - } else { - if (c !== this.form.templateContext) { - formItemContexts.set(c, number + 1); - } - } - } - : () => {}, + configProvider: inject('configProvider', ConfigConsumerProps), }; }, - inject: { - configProvider: { default: () => ConfigConsumerProps }, - }, watch: { - form() { - this.$forceUpdate(); + rules() { + if (this.validateOnRuleChange) { + this.validateFields(); + } }, }, computed: { @@ -185,100 +94,186 @@ const Form = { return this.layout === 'vertical'; }, }, - beforeUpdate() { - this.formItemContexts.forEach((number, c) => { - if (c.$forceUpdate) { - c.$forceUpdate(); - } - }); - }, - updated() { - if (this.form && this.form.cleanUpUselessFields) { - this.form.cleanUpUselessFields(); - } - }, methods: { - onSubmit(e) { - if (!getListeners(this).submit) { - e.preventDefault(); - } else { - this.$emit('submit', e); + addField(field) { + if (field) { + this.fields.push(field); } }, + removeField(field) { + if (field.fieldName) { + this.fields.splice(this.fields.indexOf(field), 1); + } + }, + handleSubmit(e) { + 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) { + const provideNameList = !!nameList; + const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : []; + if (!provideNameList) { + return this.fields; + } else { + return this.fields.filter( + field => namePathList.findIndex(namePath => isEqualName(namePath, field.fieldName)) > -1, + ); + } + }, + resetFields(name) { + if (!this.model) { + warning(false, 'Form', 'model is required for resetFields to work.'); + return; + } + this.getFieldsByNameList(name).forEach(field => { + field.resetField(); + }); + }, + clearValidate(name) { + this.getFieldsByNameList(name).forEach(field => { + field.clearValidate(); + }); + }, + handleFinishFailed(errorInfo) { + const { scrollToFirstError } = this; + this.$emit('finishFailed', errorInfo); + if (scrollToFirstError && errorInfo.errorFields.length) { + this.scrollToField(errorInfo.errorFields[0].name); + } + }, + validate() { + return this.validateField(...arguments); + }, + scrollToField(name, options = {}) { + const fields = this.getFieldsByNameList([name]); + if (fields.length) { + const fieldId = fields[0].fieldId; + const node = fieldId ? document.getElementById(fieldId) : null; + + if (node) { + scrollIntoView(node, { + scrollMode: 'if-needed', + block: 'nearest', + ...options, + }); + } + } + }, + // eslint-disable-next-line no-unused-vars + getFieldsValue(nameList = true) { + const values = {}; + this.fields.forEach(({ fieldName, fieldValue }) => { + values[fieldName] = fieldValue; + }); + if (nameList === true) { + return values; + } else { + const res = {}; + toArray(nameList).forEach(namePath => (res[namePath] = values[namePath])); + return res; + } + }, + validateFields(nameList, options) { + if (!this.model) { + warning(false, 'Form', 'model is required for validateFields to work.'); + return; + } + const provideNameList = !!nameList; + const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : []; + + // Collect result in promise list + const promiseList = []; + + this.fields.forEach(field => { + // Add field if not provide `nameList` + if (!provideNameList) { + namePathList.push(field.getNamePath()); + } + + // Skip if without rule + if (!field.getRules().length) { + return; + } + + const fieldNamePath = field.getNamePath(); + + // Add field validate rule in to promise list + if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) { + const promise = field.validateRules({ + validateMessages: { + ...defaultValidateMessages, + ...this.validateMessages, + }, + ...options, + }); + + // Wrap promise with field + promiseList.push( + promise + .then(() => ({ name: fieldNamePath, errors: [] })) + .catch(errors => + Promise.reject({ + name: fieldNamePath, + errors, + }), + ), + ); + } + }); + + const summaryPromise = allPromiseFinish(promiseList); + this.lastValidatePromise = summaryPromise; + + const returnPromise = summaryPromise + .then(() => { + if (this.lastValidatePromise === summaryPromise) { + return Promise.resolve(this.getFieldsValue(namePathList)); + } + return Promise.reject([]); + }) + .catch(results => { + const errorList = results.filter(result => result && result.errors.length); + return Promise.reject({ + values: this.getFieldsValue(namePathList), + errorFields: errorList, + outOfDate: this.lastValidatePromise !== summaryPromise, + }); + }); + + // Do not throw in console + returnPromise.catch(e => e); + + return returnPromise; + }, + validateField() { + return this.validateFields(...arguments); + }, }, render() { - const { - prefixCls: customizePrefixCls, - hideRequiredMark, - layout, - onSubmit, - $slots, - autoFormCreate, - options = {}, - } = this; + const { prefixCls: customizePrefixCls, hideRequiredMark, layout, handleSubmit } = this; const getPrefixCls = this.configProvider.getPrefixCls; const prefixCls = getPrefixCls('form', customizePrefixCls); + const { class: className, onSubmit: originSubmit, ...restProps } = this.$attrs; - const formClassName = classNames(prefixCls, { + const formClassName = classNames(prefixCls, className, { [`${prefixCls}-horizontal`]: layout === 'horizontal', [`${prefixCls}-vertical`]: layout === 'vertical', [`${prefixCls}-inline`]: layout === 'inline', [`${prefixCls}-hide-required-mark`]: hideRequiredMark, }); - if (autoFormCreate) { - warning(false, 'Form', '`autoFormCreate` is deprecated. please use `form` instead.'); - const DomForm = - this.DomForm || - createDOMForm({ - fieldNameProp: 'id', - ...options, - fieldMetaProp: FIELD_META_PROP, - fieldDataProp: FIELD_DATA_PROP, - templateContext: this.$vnode.context, - })({ - provide() { - return { - decoratorFormProps: this.$props, - }; - }, - data() { - return { - children: $slots.default, - formClassName, - submit: onSubmit, - }; - }, - created() { - autoFormCreate(this.form); - }, - render() { - const { children, formClassName, submit } = this; - return ( -
- {children} -
- ); - }, - }); - if (this.domForm) { - this.domForm.children = $slots.default; - this.domForm.submit = onSubmit; - this.domForm.formClassName = formClassName; - } - this.DomForm = DomForm; - - return ( - { - this.domForm = inst; - }} - /> - ); - } return ( -
- {$slots.default} + + {getSlot(this)}
); }, diff --git a/components/form/FormItem.jsx b/components/form/FormItem.jsx index 64744fb08..c938d64c0 100644 --- a/components/form/FormItem.jsx +++ b/components/form/FormItem.jsx @@ -1,28 +1,30 @@ -import { provide, inject, Transition } from 'vue'; +import { inject, provide, Transition } from 'vue'; +import cloneDeep from 'lodash/cloneDeep'; import PropTypes from '../_util/vue-types'; import classNames from 'classnames'; -import find from 'lodash/find'; +import getTransitionProps from '../_util/getTransitionProps'; import Row from '../grid/Row'; import Col, { ColProps } from '../grid/Col'; -import warning from '../_util/warning'; -import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; -import { +import hasProp, { initDefaultProps, - getComponent, - getSlotOptions, - isValidElement, - getAllChildren, findDOMNode, + getComponent, + getOptionProps, + getEvents, + isValidElement, getSlot, } from '../_util/props-util'; -import getTransitionProps from '../_util/getTransitionProps'; import BaseMixin from '../_util/BaseMixin'; -import { cloneElement, cloneVNodes } from '../_util/vnode'; +import { ConfigConsumerProps } 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 { ConfigConsumerProps } from '../config-provider'; +import { validateRules } from './utils/validateUtil'; +import { getNamePath } from './utils/valueUtil'; +import { toArray } from './utils/typeUtil'; +import { warning } from '../vc-util/warning'; const iconMap = { success: CheckCircleFilled, @@ -31,227 +33,256 @@ const iconMap = { validating: LoadingOutlined, }; -function noop() {} +function getPropByPath(obj, namePathList, strict) { + let tempObj = obj; -function intersperseSpace(list) { - return list.reduce((current, item) => [...current, ' ', item], []).slice(1); + const keyArr = namePathList; + let i = 0; + try { + for (let len = keyArr.length; i < len - 1; ++i) { + if (!tempObj && !strict) break; + let key = keyArr[i]; + if (key in tempObj) { + tempObj = tempObj[key]; + } else { + if (strict) { + throw Error('please transfer a valid name path to form item!'); + } + break; + } + } + if (strict && !tempObj) { + throw Error('please transfer a valid name path to form item!'); + } + } catch (error) { + console.error('please transfer a valid name path to form item!'); + } + + return { + o: tempObj, + k: keyArr[i], + v: tempObj ? tempObj[keyArr[i]] : undefined, + }; } export const FormItemProps = { id: PropTypes.string, htmlFor: PropTypes.string, prefixCls: PropTypes.string, label: PropTypes.any, - labelCol: PropTypes.shape(ColProps).loose, - wrapperCol: PropTypes.shape(ColProps).loose, help: PropTypes.any, extra: PropTypes.any, - validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']), + labelCol: PropTypes.shape(ColProps).loose, + wrapperCol: PropTypes.shape(ColProps).loose, hasFeedback: PropTypes.bool, - required: PropTypes.bool, colon: PropTypes.bool, - fieldDecoratorId: PropTypes.string, - fieldDecoratorOptions: PropTypes.object, - selfUpdate: PropTypes.bool, labelAlign: PropTypes.oneOf(['left', 'right']), + prop: PropTypes.oneOfType([Array, String, Number]), + name: PropTypes.oneOfType([Array, String, Number]), + rules: PropTypes.oneOfType([Array, Object]), + autoLink: PropTypes.bool, + required: PropTypes.bool, + validateFirst: PropTypes.bool, + validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']), }; -function comeFromSlot(vnodes = [], itemVnode) { - let isSlot = false; - for (let i = 0, len = vnodes.length; i < len; i++) { - const vnode = vnodes[i]; - if (vnode && (vnode === itemVnode || vnode.$vnode === itemVnode)) { - isSlot = true; - } else { - const componentOptions = - vnode.componentOptions || (vnode.$vnode && vnode.$vnode.componentOptions); - const children = componentOptions ? componentOptions.children : vnode.$children; - isSlot = comeFromSlot(children, itemVnode); - } - if (isSlot) { - break; - } - } - return isSlot; -} export default { name: 'AFormItem', mixins: [BaseMixin], inheritAttrs: false, - __ANT_FORM_ITEM: true, + __ANT_NEW_FORM_ITEM: true, props: initDefaultProps(FormItemProps, { hasFeedback: false, + autoLink: true, }), setup() { return { isFormItemChildren: inject('isFormItemChildren', false), - FormContext: inject('FormContext', {}), - decoratorFormProps: inject('decoratorFormProps', {}), - collectFormItemContext: inject('collectFormItemContext', noop), configProvider: inject('configProvider', ConfigConsumerProps), + FormContext: inject('FormContext', {}), }; }, data() { - return { helpShow: false }; + warning(!hasProp(this, 'prop'), `\`prop\` is deprecated. Please use \`name\` instead.`); + return { + validateState: this.validateStatus, + validateMessage: '', + validateDisabled: false, + validator: {}, + helpShow: false, + errors: [], + }; }, + computed: { - itemSelfUpdate() { - return !!(this.selfUpdate === undefined ? this.FormContext.selfUpdate : this.selfUpdate); + fieldName() { + return this.name || this.prop; + }, + namePath() { + return getNamePath(this.fieldName); + }, + fieldId() { + if (this.id) { + return this.id; + } else if (!this.namePath.length) { + return undefined; + } else { + const formName = this.FormContext.name; + const mergedId = this.namePath.join('_'); + return formName ? `${formName}_${mergedId}` : mergedId; + } + }, + fieldValue() { + const model = this.FormContext.model; + if (!model || !this.fieldName) { + return; + } + return getPropByPath(model, this.namePath, true).v; + }, + isRequired() { + let rules = this.getRules(); + let isRequired = false; + if (rules && rules.length) { + rules.every(rule => { + if (rule.required) { + isRequired = true; + return false; + } + return true; + }); + } + return isRequired || this.required; + }, + }, + watch: { + validateStatus(val) { + this.validateState = val; }, }, created() { provide('isFormItemChildren', true); - this.collectContext(); }, - beforeUpdate() { - if (process.env.NODE_ENV !== 'production') { - this.collectContext(); + mounted() { + if (this.fieldName) { + const { addField } = this.FormContext; + addField && addField(this); + this.initialValue = cloneDeep(this.fieldValue); } }, beforeUnmount() { - this.collectFormItemContext(this.$vnode && this.$vnode.context, 'delete'); - }, - mounted() { - const { help, validateStatus } = this.$props; - warning( - this.getControls(this.slotDefault, true).length <= 1 || - help !== undefined || - validateStatus !== undefined, - 'Form.Item', - 'Cannot generate `validateStatus` and `help` automatically, ' + - 'while there are more than one `getFieldDecorator` in it.', - ); - warning( - !this.fieldDecoratorId, - 'Form.Item', - '`fieldDecoratorId` is deprecated. please use `v-decorator={id, options}` instead.', - ); + const { removeField } = this.FormContext; + removeField && removeField(this); }, methods: { - collectContext() { - if (this.FormContext.form && this.FormContext.form.templateContext) { - const { templateContext } = this.FormContext.form; - const vnodes = Object.values(templateContext.$slots || {}).reduce((a, b) => { - return [...a, ...b]; - }, []); - const isSlot = comeFromSlot(vnodes, this.$vnode); - warning(!isSlot, 'You can not set FormItem from slot, please use slot-scope instead slot'); - let isSlotScope = false; - // 进一步判断是否是通过slot-scope传递 - if (!isSlot && this.$vnode.context !== templateContext) { - isSlotScope = comeFromSlot(this.$vnode.context.$children, templateContext.$vnode); - } - if (!isSlotScope && !isSlot) { - this.collectFormItemContext(this.$vnode.context); - } + getNamePath() { + const { fieldName } = this; + const { prefixName = [] } = this.FormContext; + + return fieldName !== undefined ? [...prefixName, ...this.namePath] : []; + }, + validateRules(options) { + const { validateFirst = false, messageVariables } = this.$props; + const { triggerName } = options || {}; + const namePath = this.getNamePath(); + + let filteredRules = this.getRules(); + if (triggerName) { + filteredRules = filteredRules.filter(rule => { + const { trigger } = rule; + if (!trigger) { + return true; + } + const triggerList = toArray(trigger); + return triggerList.includes(triggerName); + }); } + if (!filteredRules.length) { + return Promise.resolve(); + } + const promise = validateRules( + namePath, + this.fieldValue, + filteredRules, + options, + validateFirst, + messageVariables, + ); + this.validateState = 'validating'; + this.errors = []; + + promise + .catch(e => e) + .then((errors = []) => { + if (this.validateState === 'validating') { + this.validateState = errors.length ? 'error' : 'success'; + this.validateMessage = errors[0]; + this.errors = errors; + } + }); + + return promise; + }, + getRules() { + let formRules = this.FormContext.rules; + const selfRules = this.rules; + const requiredRule = + this.required !== undefined ? { required: !!this.required, trigger: 'change' } : []; + const prop = getPropByPath(formRules, this.namePath); + formRules = formRules ? prop.o[prop.k] || prop.v : []; + return [].concat(selfRules || formRules || []).concat(requiredRule); + }, + getFilteredRule(trigger) { + const rules = this.getRules(); + return rules + .filter(rule => { + if (!rule.trigger || trigger === '') return true; + if (Array.isArray(rule.trigger)) { + return rule.trigger.indexOf(trigger) > -1; + } else { + return rule.trigger === trigger; + } + }) + .map(rule => ({ ...rule })); + }, + onFieldBlur() { + this.validateRules({ triggerName: 'blur' }); + }, + onFieldChange() { + if (this.validateDisabled) { + this.validateDisabled = 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; + if (Array.isArray(value)) { + prop.o[prop.k] = [].concat(this.initialValue); + } else { + prop.o[prop.k] = this.initialValue; + } + // reset validateDisabled after onFieldChange triggered + this.$nextTick(() => { + this.validateDisabled = false; + }); }, getHelpMessage() { const help = getComponent(this, 'help'); - const onlyControl = this.getOnlyControl(); - if (help === undefined && onlyControl) { - const errors = this.getField().errors; - if (errors) { - return intersperseSpace( - errors.map((e, index) => { - let node = null; - if (isValidElement(e)) { - node = e; - } else if (isValidElement(e.message)) { - node = e.message; - } - return node ? cloneElement(node, { key: index }) : e.message; - }), - ); - } else { - return ''; - } - } - return help; + return this.validateMessage || help; }, - getControls(childrenArray = [], recursively) { - let controls = []; - for (let i = 0; i < childrenArray.length; i++) { - if (!recursively && controls.length > 0) { - break; - } - - const child = childrenArray[i]; - // if (!child.tag && child.text.trim() === '') { - // continue; - // } - - if (typeof child.type === 'object' && child.type.__ANT_FORM_ITEM) { - continue; - } - const children = getAllChildren(child); - const attrs = child.props || {}; - if (FIELD_META_PROP in attrs) { - // And means FIELD_DATA_PROP in child.props, too. - controls.push(child); - } else if (children) { - controls = controls.concat(this.getControls(children, recursively)); - } - } - return controls; - }, - - getOnlyControl() { - const child = this.getControls(this.slotDefault, false)[0]; - return child !== undefined ? child : null; - }, - - getChildAttr(prop) { - const child = this.getOnlyControl(); - let data = {}; - if (!child) { - return undefined; - } - debugger; - if (child.data) { - data = child.data; - } else if (child.$vnode && child.$vnode.data) { - data = child.$vnode.data; - } - return data[prop] || data.attrs[prop]; - }, - - getId() { - return this.getChildAttr('id'); - }, - - getMeta() { - return this.getChildAttr(FIELD_META_PROP); - }, - - getField() { - return this.getChildAttr(FIELD_DATA_PROP); - }, - - getValidateStatus() { - const onlyControl = this.getOnlyControl(); - if (!onlyControl) { - return ''; - } - const field = this.getField(); - if (field.validating) { - return 'validating'; - } - if (field.errors) { - return 'error'; - } - const fieldValue = 'value' in field ? field.value : this.getMeta().initialValue; - if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { - return 'success'; - } - return ''; - }, - - // Resolve duplicated ids bug between different forms - // https://github.com/ant-design/ant-design/issues/7351 onLabelClick() { - const id = this.id || this.getId(); + const id = this.fieldId; if (!id) { return; } @@ -269,24 +300,6 @@ export default { } }, - isRequired() { - const { required } = this; - if (required !== undefined) { - return required; - } - if (this.getOnlyControl()) { - const meta = this.getMeta() || {}; - const validate = meta.validate || []; - - return validate - .filter(item => !!item.rules) - .some(item => { - return item.rules.some(rule => rule.required); - }); - } - return false; - }, - renderHelp(prefixCls) { const help = this.getHelpMessage(); const children = help ? ( @@ -314,17 +327,12 @@ export default { }, renderValidateWrapper(prefixCls, c1, c2, c3) { - const props = this.$props; - const onlyControl = this.getOnlyControl; - const validateStatus = - props.validateStatus === undefined && onlyControl - ? this.getValidateStatus() - : props.validateStatus; + const validateStatus = this.validateState; let classes = `${prefixCls}-item-control`; if (validateStatus) { classes = classNames(`${prefixCls}-item-control`, { - 'has-feedback': props.hasFeedback || validateStatus === 'validating', + 'has-feedback': this.hasFeedback || validateStatus === 'validating', 'has-success': validateStatus === 'success', 'has-warning': validateStatus === 'warning', 'has-error': validateStatus === 'error', @@ -334,7 +342,7 @@ export default { const IconNode = validateStatus && iconMap[validateStatus]; const icon = - props.hasFeedback && IconNode ? ( + this.hasFeedback && IconNode ? ( @@ -374,9 +382,9 @@ export default { labelCol: contextLabelCol, colon: contextColon, } = this.FormContext; - const { labelAlign, labelCol, colon, id, htmlFor } = this; + const { labelAlign, labelCol, colon, fieldId, htmlFor } = this; const label = getComponent(this, 'label'); - const required = this.isRequired(); + const required = this.isRequired; const mergedLabelCol = labelCol || contextLabelCol || {}; const mergedLabelAlign = labelAlign || contextLabelAlign; @@ -416,7 +424,7 @@ export default { return label ? (