UNPKG

zent

Version:

一套前端设计语言和基于React的实现

463 lines (395 loc) 14.5 kB
/* eslint-disable no-underscore-dangle */ import { Component, PropTypes, createElement } from 'react'; import omit from 'zent-utils/lodash/omit'; import find from 'zent-utils/lodash/find'; import noop from 'zent-utils/lodash/noop'; import assign from 'zent-utils/lodash/assign'; import isEqual from 'zent-utils/lodash/isEqual'; import some from 'zent-utils/lodash/some'; import isPromise from 'zent-utils/isPromise'; import { getDisplayName, silenceEvent, silenceEvents } from './utils'; import rules from './validationRules'; import handleSubmit from './handleSubmit'; const emptyArray = []; const checkSubmit = submit => { if (!submit || typeof submit !== 'function') { throw new Error('You must either pass handleSubmit() an onSubmit function or pass onSubmit as a prop'); } return submit; }; const createForm = (config = {}) => { const { formValidations } = config; const validationRules = assign({}, rules, formValidations); return WrappedComponent => { return class Form extends Component { constructor(props) { super(props); this.state = { isFormValid: true, isSubmitting: false, }; this.fields = []; this._isMounted = false; } static displayName = `Form(${getDisplayName(WrappedComponent)})` static WrappedComponent = WrappedComponent static propTypes = { onSubmit: PropTypes.func, onSubmitSuccess: PropTypes.func, onSubmitFail: PropTypes.func, onValid: PropTypes.func, onInvalid: PropTypes.func, onChange: PropTypes.func, validationErrors: PropTypes.object } static defaultProps = { onSubmit: noop, onSubmitSuccess: noop, onSubmitFail: noop, onValid: noop, onInvalid: noop, onChange: noop, validationErrors: null } static childContextTypes = { zentForm: PropTypes.object } getChildContext() { return { zentForm: { attachToForm: this.attachToForm, detachFromForm: this.detachFromForm, validate: this.validate, asyncValidate: this.asyncValidate, getFormValues: this.getFormValues, getFieldError: this.getFieldError, isValidValue: this.isValidValue, setFieldExternalErrors: this.setFieldExternalErrors, resetFieldsValue: this.resetFieldsValue, setFormPristine: this.setFormPristine, isValid: this.isValid, isSubmitting: this.isSubmitting } }; } componentDidMount() { this.validateForm(); this._isMounted = true; } componentWillUpdate() { this.prevFieldNames = this.fields.map(field => field.props.name); } componentDidUpdate() { const { validationErrors } = this.props; if (validationErrors && typeof validationErrors === 'object' && Object.keys(validationErrors).length > 0) { this.setFieldValidationErrors(validationErrors); } const newFieldNames = this.fields.map(field => field.props.name); if (!isEqual(this.prevFieldNames, newFieldNames)) { this.validateForm(); } } submitCompleted = (result) => { delete this.submitPromise; return result; } submitFailed = (error) => { delete this.submitPromise; throw error; } listenToSubmit = (promise) => { if (!isPromise(promise)) { return promise; } // 当submit是一个promise时,需要一个标识表明正在提交,提交结束后删除标识 this.submitPromise = promise; return promise.then(this.submitCompleted, this.submitFailed); } submit = (submitOrEvent) => { const { onSubmit } = this.props; // 在表单中手动调用handleSubmit或者把handleSubmit赋值给表单的onSubmit回调 // handleSubmit赋值给表单的onSubmit时,submitOrEvent是一个event对象 // handleSubmit的参数必须是function if (!submitOrEvent || silenceEvent(submitOrEvent)) { if (!this.submitPromise) { // 调用props传入的onSubmit方法 return this.listenToSubmit(handleSubmit(checkSubmit(onSubmit), this)); } } else { // submitOrEvent是一个自定义submit函数,返回一个promise对象 return silenceEvents(() => !this.submitPromise && this.listenToSubmit(handleSubmit(checkSubmit(submitOrEvent), this))); } } isSubmitting = () => { return this.state.isSubmitting; } isValid = () => { return this.state.isFormValid; } setFieldValidationErrors = (errors) => { this.fields.forEach(field => { const name = field.props.name; field.setState({ _isValid: !(name in errors), _validationError: typeof errors[name] === 'string' ? [errors[name]] : errors[name] }); }); } // 设置服务端返回的错误信息 setFieldExternalErrors = (errors) => { Object.keys(errors).forEach((name) => { const field = find(this.fields, (component) => component.props.name === name); if (!field) { throw new Error(`field ${name} does not exits`); } field.setState({ _isValid: false, _externalError: typeof errors[name] === 'string' ? [errors[name]] : errors[name] }); }); } setFormPristine = (isPristine) => { this.fields.forEach(field => { field.setState({ _isPristine: isPristine }); }); } resetFieldsValue= (data) => { this.fields.forEach(field => { const name = field.props.name; if (data && data.hasOwnProperty(name)) { field.setValue(data[name]); } else { field.resetValue(); } }); } reset = (data) => { this.setFormPristine(true); this.resetFieldsValue(data); } isFieldTouched = (name) => { const field = this.fields.find(component => component.props.name === name); if (!field) return false; return !field.isPristine(); } isFieldValidating = (name) => { const field = this.fields.find(component => component.props.name === name); if (!field) return false; return field.isValidating(); } getFieldError = (name) => { const field = this.fields.find(component => component.props.name === name); if (!field) return ''; return field.getErrorMessage(); } getFormValues = () => { return this.fields.reduce((values, field) => { const name = field.props.name; values[name] = field.getValue(); return values; }, {}); } getValidationErrors = () => { return this.fields.reduce((errors, field) => { const name = field.props.name; errors[name] = field.getErrorMessage(); return errors; }, {}); } getPristineValues = () => { return this.fields.reduce((values, field) => { const name = field.props.name; values[name] = field.getPristineValue(); return values; }, {}); } isChanged = () => { return !isEqual(this.getPristineValues(), this.getFormValues()); } isValidating = () => { return some(this.fields, (field) => { return field.isValidating(); }); } isValidValue = (field, value) => { return this.runValidation(field, value).isValid; } runValidation = (field, value = field.getValue()) => { const formValidationErrors = this.props.validationErrors; const { name, validationError, validationErrors } = field.props; const currentValues = this.getFormValues(); const validationResults = this.runRules(value, currentValues, field._validations); const isValid = !validationResults.failed.length && !(formValidationErrors && formValidationErrors[field.props.name]); return { isValid, error: (function () { if (isValid) { return emptyArray; } if (validationResults.errors.length) { return validationResults.errors; } if (formValidationErrors && formValidationErrors[name]) { return typeof formValidationErrors[name] === 'string' ? [formValidationErrors[name]] : formValidationErrors[name]; } if (validationResults.failed.length) { return validationResults.failed.map((failed) => { return validationErrors[failed] ? validationErrors[failed] : validationError; }).filter((x, pos, arr) => { // Remove duplicates return arr.indexOf(x) === pos; }); } }()) }; } runRules = (value, currentValues, validations = []) => { const results = { errors: [], failed: [] }; function updateResults(validation, validationMethod) { // validation方法可以直接返回错误信息,否则需要返回布尔值表明校验是否成功 if (typeof validation === 'string') { results.errors.push(validation); results.failed.push(validationMethod); } else if (!validation) { results.failed.push(validationMethod); } } Object.keys(validations).forEach((validationMethod) => { // validations中不指定function则必须是内置的rule if (!validationRules[validationMethod] && typeof validations[validationMethod] !== 'function') { throw new Error(`does not have the validation rule: ${validationMethod}`); } // 使用自定义校验方法或内置校验方法(可以按需添加) if (typeof validations[validationMethod] === 'function') { const validation = validations[validationMethod](currentValues, value); updateResults(validation, validationMethod); } else if (typeof validations[validationMethod] !== 'function') { const validation = validationRules[validationMethod](currentValues, value, validations[validationMethod]); updateResults(validation, validationMethod); } }); return results; } validate = (field) => { // 初始化时调用validate不触发onChange if (this._isMounted) { this.props.onChange(this.getFormValues(), this.isChanged()); } const validation = this.runValidation(field); field.setState({ _isValid: validation.isValid, _validationError: validation.error, _externalError: null }, this.validateForm); } asyncValidate = (field, value) => { const { asyncValidation } = field.props; const values = this.getFormValues(); if (field.state._validationError.length) return; field.setState({ _isValidating: true }); const promise = asyncValidation(values, value); if (!isPromise(promise)) { throw new Error('asyncValidation function must return a promise'); } const handleResult = rejected => error => { field.setState({ _isValidating: false, _isValid: !rejected, _externalError: error ? [error] : null }); if (rejected) { this.setState({ isFormValid: false }); } }; return promise.then(handleResult(false), handleResult(true)); } validateForm = () => { const onValidationComplete = () => { const allIsValid = this.fields.every(field => { return field.isValid(); }); this.setState({ isFormValid: allIsValid }); if (allIsValid) { this.props.onValid(); } else { this.props.onInvalid(); } }; this.fields.forEach((field, index) => { const { _externalError } = field.state; const validation = this.runValidation(field); if (validation.isValid && (_externalError)) { validation.isValid = false; } field.setState({ _isValid: validation.isValid, _validationError: validation.error, _externalError: !validation.isValid && _externalError ? _externalError : null }, index === this.fields.length - 1 ? onValidationComplete : null); }); } attachToForm = (field) => { if (this.fields.indexOf(field) < 0) { this.fields.push(field); } // form初始化时不校验,后续动态添加的元素再校验 this._isMounted && this.validate(field); } detachFromForm = (field) => { const fieldPos = this.fields.indexOf(field); if (fieldPos >= 0) { this.fields.splice(fieldPos, 1); } this.validateForm(); } render() { const passableProps = omit(this.props, [ 'validationErrors', 'handleSubmit', 'onChange' ]); return createElement(WrappedComponent, { ...passableProps, handleSubmit: this.submit, zentForm: { getFormValues: this.getFormValues, getFieldError: this.getFieldError, setFieldExternalErrors: this.setFieldExternalErrors, resetFieldsValue: this.resetFieldsValue, setFormPristine: this.setFormPristine, isFieldTouched: this.isFieldTouched, isFieldValidating: this.isFieldValidating, isValid: this.isValid, isValidating: this.isValidating, isSubmitting: this.isSubmitting } }); } }; }; }; export default createForm;