From 346ab47eb65ff11acf5d06b4220cc52cf87917d9 Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Mon, 13 Jul 2020 18:56:31 +0800 Subject: [PATCH] refactor: form --- components/form-model/Form.jsx | 60 ++-- components/form-model/FormItem.jsx | 438 ++++++++++++++++++++++++++--- components/form-model/messages.js | 49 ++++ components/form-model/utils.js | 39 +++ 4 files changed, 518 insertions(+), 68 deletions(-) create mode 100644 components/form-model/messages.js diff --git a/components/form-model/Form.jsx b/components/form-model/Form.jsx index c2e1da528..cd6e84848 100755 --- a/components/form-model/Form.jsx +++ b/components/form-model/Form.jsx @@ -6,7 +6,7 @@ import { ColProps } from '../grid/Col'; import isRegExp from 'lodash/isRegExp'; import warning from '../_util/warning'; import FormItem from './FormItem'; -import { initDefaultProps, getListeners, getSlot } from '../_util/props-util'; +import { initDefaultProps, getSlot } from '../_util/props-util'; import { ConfigConsumerProps } from '../config-provider'; import { getParams } from './utils'; @@ -22,6 +22,11 @@ export const FormProps = { rules: PropTypes.object, validateMessages: PropTypes.any, validateOnRuleChange: PropTypes.bool, + // 提交失败自动滚动到第一个错误字段 + scrollToFirstError: PropTypes.bool, + onFinish: PropTypes.func, + onFinishFailed: PropTypes.func, + name: PropTypes.name, }; export const ValidationRule = { @@ -47,8 +52,6 @@ export const ValidationRule = { transform: PropTypes.func, /** custom validate function (Note: callback must be called) */ validator: PropTypes.func, - // 提交失败自动滚动到第一个错误字段 - scrollToFirstError: PropTypes.bool, }; const Form = { @@ -93,12 +96,18 @@ const Form = { this.fields.splice(this.fields.indexOf(field), 1); } }, - onSubmit(e) { - if (!getListeners(this).submit) { - e.preventDefault(); - } else { - this.$emit('submit', e); - } + handleSubmit(e) { + e.preventDefault(); + e.stopPropagation(); + this.$emit('submit', e); + const res = this.validate(); + res + .then(values => { + this.$emit('finish', values); + }) + .catch(errors => { + this.handleFinishFailed(errors); + }); }, resetFields(props = []) { if (!this.model) { @@ -124,8 +133,16 @@ const Form = { 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); + // if (!this.model) { // warning(false, 'FormModel', 'model is required for resetFields to work.'); // return; @@ -179,11 +196,11 @@ const Form = { let { callback } = params; if (!callback || typeof callback === 'function') { const oldCb = callback; - callback = (errors, values) => { + callback = (errorFields, values) => { if (oldCb) { - oldCb(errors, values); - } else if (errors) { - reject({ errors, values }); + oldCb(errorFields, values); + } else if (errorFields) { + reject({ errorFields, values }); } else { resolve(values); } @@ -208,8 +225,9 @@ const Form = { let fieldsErrors = {}; let valid = true; let count = 0; + const promiseList = []; fields.forEach(field => { - field.validate('', errors => { + const promise = field.validate('', errors => { if (errors) { valid = false; fieldsErrors[field.prop] = errors; @@ -219,6 +237,7 @@ const Form = { callback(valid ? null : fieldsErrors, this.getFieldsValue(fields)); } }); + promiseList.push(promise.then(() => {})); }); }); pending.catch(e => { @@ -228,20 +247,11 @@ const Form = { return e; }); return pending; - // names = [].concat(names); - // const fields = this.fields.filter(field => names.indexOf(field.prop) !== -1); - // if (!fields.length) { - // warning(false, 'FormModel', 'please pass correct props!'); - // return; - // } - // fields.forEach(field => { - // field.validate('', cb); - // }); }, }, render() { - const { prefixCls: customizePrefixCls, hideRequiredMark, layout, onSubmit } = 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; @@ -253,7 +263,7 @@ const Form = { [`${prefixCls}-hide-required-mark`]: hideRequiredMark, }); return ( -
); diff --git a/components/form-model/FormItem.jsx b/components/form-model/FormItem.jsx index cd6fa7a4d..1b19b3bf5 100644 --- a/components/form-model/FormItem.jsx +++ b/components/form-model/FormItem.jsx @@ -1,10 +1,14 @@ -import { inject } from 'vue'; +import { inject, provide, Transition } from 'vue'; import AsyncValidator from 'async-validator'; import cloneDeep from 'lodash/cloneDeep'; import PropTypes from '../_util/vue-types'; -import { ColProps } from '../grid/Col'; +import classNames from 'classnames'; +import getTransitionProps from '../_util/getTransitionProps'; +import Row from '../grid/Row'; +import Col, { ColProps } from '../grid/Col'; import { initDefaultProps, + findDOMNode, getComponent, getOptionProps, getEvents, @@ -13,10 +17,20 @@ import { } from '../_util/props-util'; import BaseMixin from '../_util/BaseMixin'; import { ConfigConsumerProps } from '../config-provider'; -import FormItem from '../form/FormItem'; 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 { finishOnAllFailed, finishOnFirstFailed, getNamePath } from './utils'; +import { warning } from '../vc-util/warning'; -function noop() {} +const iconMap = { + success: CheckCircleFilled, + warning: ExclamationCircleFilled, + error: CloseCircleFilled, + validating: LoadingOutlined, +}; function getPropByPath(obj, path, strict) { let tempObj = obj; @@ -74,6 +88,7 @@ export default { }), setup() { return { + isFormItemChildren: inject('isFormItemChildren', false), configProvider: inject('configProvider', ConfigConsumerProps), FormContext: inject('FormContext', {}), }; @@ -84,10 +99,16 @@ export default { validateMessage: '', validateDisabled: false, validator: {}, + helpShow: false, }; }, computed: { + fieldId() { + return this.id || (this.FormContext.name && this.prop) + ? `${this.FormContext.name}_${this.prop}` + : undefined; + }, fieldValue() { const model = this.FormContext.model; if (!model || !this.prop) { @@ -111,7 +132,7 @@ export default { return true; }); } - return isRequired; + return isRequired || this.required; }, }, watch: { @@ -119,6 +140,9 @@ export default { this.validateState = val; }, }, + created() { + provide('isFormItemChildren', true); + }, mounted() { if (this.prop) { const { addField } = this.FormContext; @@ -131,35 +155,188 @@ export default { removeField && removeField(this); }, methods: { - validate(trigger, callback = noop) { + async validateRule(name, value, rule) { + const cloneRule = { ...rule }; + // We should special handle array validate + let subRuleField = null; + if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) { + subRuleField = cloneRule.defaultField; + delete cloneRule.defaultField; + } + let result = []; + const validator = new AsyncValidator({ + [name]: [cloneRule], + }); + if (this.FormContext && this.FormContext.validateMessages) { + validator.messages(this.FormContext.validateMessages); + } + try { + await validator.validate( + { [this.prop]: this.fieldValue }, + { firstFields: !!this.validateFirst }, + ); + } catch (errObj) { + if (errObj.errors) { + result = errObj.errors.map(({ message }) => message); + } else { + console.error(errObj); + } + } + if (!result.length && subRuleField) { + const subResults = await Promise.all( + value.map((subValue, i) => this.validateRule(`${name}.${i}`, subValue, subRuleField)), + ); + + return subResults.reduce((prev, errors) => [...prev, ...errors], []); + } + return result; + }, + validateRules(namePath, value, rules, validateFirst) { + const name = namePath.join('.'); + + // Fill rule with context + const filledRules = rules.map(currentRule => { + const originValidatorFunc = currentRule.validator; + + if (!originValidatorFunc) { + return currentRule; + } + return { + ...currentRule, + validator(rule, val, callback) { + let hasPromise = false; + + // Wrap callback only accept when promise not provided + const wrappedCallback = (...args) => { + // Wait a tick to make sure return type is a promise + Promise.resolve().then(() => { + warning( + !hasPromise, + 'Your validator function has already return a promise. `callback` will be ignored.', + ); + + if (!hasPromise) { + callback(...args); + } + }); + }; + + // Get promise + const promise = originValidatorFunc(rule, val, wrappedCallback); + hasPromise = + promise && typeof promise.then === 'function' && typeof promise.catch === 'function'; + + /** + * 1. Use promise as the first priority. + * 2. If promise not exist, use callback with warning instead + */ + warning(hasPromise, '`callback` is deprecated. Please return a promise instead.'); + + if (hasPromise) { + promise + .then(() => { + callback(); + }) + .catch(err => { + callback(err); + }); + } + }, + }; + }); + + let summaryPromise; + + if (validateFirst === true) { + // >>>>> Validate by serialization + summaryPromise = new Promise(async resolve => { + /* eslint-disable no-await-in-loop */ + for (let i = 0; i < filledRules.length; i += 1) { + const errors = await this.validateRule(name, value, filledRules[i]); + if (errors.length) { + resolve(errors); + return; + } + } + /* eslint-enable */ + + resolve([]); + }); + } else { + // >>>>> Validate by parallel + const rulePromises = filledRules.map(rule => this.validateRule(name, value, rule)); + + summaryPromise = (validateFirst + ? finishOnFirstFailed(rulePromises) + : finishOnAllFailed(rulePromises) + ).then(errors => { + if (!errors.length) { + return []; + } + + return Promise.reject(errors); + }); + } + + // Internal catch error to avoid console error log. + summaryPromise.catch(e => e); + + return summaryPromise; + }, + validate(trigger) { this.validateDisabled = false; const rules = this.getFilteredRule(trigger); if (!rules || rules.length === 0) { - callback(); - return true; + return; } this.validateState = 'validating'; - const descriptor = {}; if (rules && rules.length > 0) { rules.forEach(rule => { delete rule.trigger; }); } - descriptor[this.prop] = rules; - const validator = new AsyncValidator(descriptor); - if (this.FormContext && this.FormContext.validateMessages) { - validator.messages(this.FormContext.validateMessages); - } - const model = {}; - model[this.prop] = this.fieldValue; - validator.validate(model, {}, errors => { - this.validateState = errors ? 'error' : 'success'; - this.validateMessage = errors ? errors[0].message : ''; - callback(errors); - this.FormContext && - this.FormContext.$emit && - this.FormContext.$emit('validate', this.prop, !errors, this.validateMessage || null); - }); + // descriptor[this.prop] = rules; + // const validator = new AsyncValidator(descriptor); + // if (this.FormContext && this.FormContext.validateMessages) { + // validator.messages(this.FormContext.validateMessages); + // } + const fieldNamePath = getNamePath(this.prop); + // const promiseList = []; + const promise = this.validateRules(fieldNamePath, this.fieldValue, rules, this.validateFirst); + promise + .then(res => { + // eslint-disable-next-line no-console + console.log(res); + this.validateState = 'success'; + this.validateMessage = ''; + return { name: fieldNamePath, errors: [] }; + }) + .catch(errors => { + this.validateState = 'error'; + this.validateMessage = errors; + Promise.reject({ + name: fieldNamePath, + errors, + }); + }); + return promise; + // // Wrap promise with field + // promiseList.push( + // promise + // .then(() => ({ name: fieldNamePath, errors: [] })) + // .catch(errors => + // Promise.reject({ + // name: fieldNamePath, + // errors, + // }), + // ), + // ); + // this.validateState = result.length ? 'error' : 'success'; + // this.validateMessage = result.length ? result[0] : ''; + // this.FormContext && + // this.FormContext.$emit && + // this.FormContext.$emit('validate', this.prop, !result.length, this.validateMessage || null); + // return result; }, getRules() { let formRules = this.FormContext.rules; @@ -219,21 +396,200 @@ export default { 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 ? ( +