From b5cd8f57141099130dc135d0bba3d7cc6e1196ee Mon Sep 17 00:00:00 2001 From: tjz <415800467@qq.com> Date: Wed, 2 May 2018 21:35:42 +0800 Subject: [PATCH] feat: init vc-form component --- components/vc-form/index.js | 3 + components/vc-form/src/createBaseForm.jsx | 529 +++++++++++++++++++ components/vc-form/src/createDOMForm.jsx | 104 ++++ components/vc-form/src/createFieldsStore.jsx | 261 +++++++++ components/vc-form/src/createForm.jsx | 32 ++ components/vc-form/src/createFormField.jsx | 16 + components/vc-form/src/index.jsx | 6 + components/vc-form/src/propTypes.js | 24 + components/vc-form/src/utils.js | 153 ++++++ 9 files changed, 1128 insertions(+) create mode 100644 components/vc-form/index.js create mode 100644 components/vc-form/src/createBaseForm.jsx create mode 100644 components/vc-form/src/createDOMForm.jsx create mode 100644 components/vc-form/src/createFieldsStore.jsx create mode 100644 components/vc-form/src/createForm.jsx create mode 100644 components/vc-form/src/createFormField.jsx create mode 100644 components/vc-form/src/index.jsx create mode 100644 components/vc-form/src/propTypes.js create mode 100644 components/vc-form/src/utils.js diff --git a/components/vc-form/index.js b/components/vc-form/index.js new file mode 100644 index 000000000..93b8930ae --- /dev/null +++ b/components/vc-form/index.js @@ -0,0 +1,3 @@ +// export this package's api +import { createForm, createFormField } from './src/' +export { createForm, createFormField } diff --git a/components/vc-form/src/createBaseForm.jsx b/components/vc-form/src/createBaseForm.jsx new file mode 100644 index 000000000..78a0a4b4a --- /dev/null +++ b/components/vc-form/src/createBaseForm.jsx @@ -0,0 +1,529 @@ +import createReactClass from 'create-react-class' +import AsyncValidator from 'async-validator' +import warning from 'warning' +import get from 'lodash/get' +import set from 'lodash/set' +import createFieldsStore from './createFieldsStore' +import { cloneElement } from '../../_util/vnode' +import { + argumentContainer, + identity, + normalizeValidateRules, + getValidateTriggers, + getValueFromEvent, + hasRules, + getParams, + isEmptyObject, + flattenArray, +} from './utils' + +const DEFAULT_TRIGGER = 'onChange' + +function createBaseForm (option = {}, mixins = []) { + const { + validateMessages, + onFieldsChange, + onValuesChange, + mapProps = identity, + mapPropsToFields, + fieldNameProp, + fieldMetaProp, + fieldDataProp, + formPropName = 'form', + // @deprecated + withRef, + } = option + + return function decorate (WrappedComponent) { + const Form = createReactClass({ + mixins, + + getInitialState () { + const fields = mapPropsToFields && mapPropsToFields(this.props) + this.fieldsStore = createFieldsStore(fields || {}) + + this.instances = {} + this.cachedBind = {} + this.clearedFieldMetaCache = {}; + // HACK: https://github.com/ant-design/ant-design/issues/6406 + ['getFieldsValue', + 'getFieldValue', + 'setFieldsInitialValue', + 'getFieldsError', + 'getFieldError', + 'isFieldValidating', + 'isFieldsValidating', + 'isFieldsTouched', + 'isFieldTouched'].forEach(key => { + this[key] = (...args) => { + if (process.env.NODE_ENV !== 'production') { + warning( + false, + 'you should not use `ref` on enhanced form, please use `wrappedComponentRef`. ' + + 'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140' + ) + } + return this.fieldsStore[key](...args) + } + }) + + return { + submitting: false, + } + }, + + componentWillReceiveProps (nextProps) { + if (mapPropsToFields) { + this.fieldsStore.updateFields(mapPropsToFields(nextProps)) + } + }, + + onCollectCommon (name, action, args) { + const fieldMeta = this.fieldsStore.getFieldMeta(name) + if (fieldMeta[action]) { + fieldMeta[action](...args) + } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) { + fieldMeta.originalProps[action](...args) + } + const value = fieldMeta.getValueFromEvent + ? fieldMeta.getValueFromEvent(...args) + : getValueFromEvent(...args) + if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) { + const valuesAll = this.fieldsStore.getAllValues() + const valuesAllSet = {} + valuesAll[name] = value + Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key])) + onValuesChange(this.props, set({}, name, value), valuesAllSet) + } + const field = this.fieldsStore.getField(name) + return ({ name, field: { ...field, value, touched: true }, fieldMeta }) + }, + + onCollect (name_, action, ...args) { + const { name, field, fieldMeta } = this.onCollectCommon(name_, action, args) + const { validate } = fieldMeta + const newField = { + ...field, + dirty: hasRules(validate), + } + this.setFields({ + [name]: newField, + }) + }, + + onCollectValidate (name_, action, ...args) { + const { field, fieldMeta } = this.onCollectCommon(name_, action, args) + const newField = { + ...field, + dirty: true, + } + this.validateFieldsInternal([newField], { + action, + options: { + firstFields: !!fieldMeta.validateFirst, + }, + }) + }, + + getCacheBind (name, action, fn) { + if (!this.cachedBind[name]) { + this.cachedBind[name] = {} + } + const cache = this.cachedBind[name] + if (!cache[action]) { + cache[action] = fn.bind(this, name, action) + } + return cache[action] + }, + + recoverClearedField (name) { + if (this.clearedFieldMetaCache[name]) { + this.fieldsStore.setFields({ + [name]: this.clearedFieldMetaCache[name].field, + }) + this.fieldsStore.setFieldMeta(name, this.clearedFieldMetaCache[name].meta) + delete this.clearedFieldMetaCache[name] + } + }, + + getFieldDecorator (name, fieldOption) { + const props = this.getFieldProps(name, fieldOption) + return (fieldElem) => { + const fieldMeta = this.fieldsStore.getFieldMeta(name) + const originalProps = fieldElem.props + if (process.env.NODE_ENV !== 'production') { + const valuePropName = fieldMeta.valuePropName + warning( + !(valuePropName in originalProps), + `\`getFieldDecorator\` will override \`${valuePropName}\`, ` + + `so please don't set \`${valuePropName}\` directly ` + + `and use \`setFieldsValue\` to set it.` + ) + const defaultValuePropName = + `default${valuePropName[0].toUpperCase()}${valuePropName.slice(1)}` + warning( + !(defaultValuePropName in originalProps), + `\`${defaultValuePropName}\` is invalid ` + + `for \`getFieldDecorator\` will set \`${valuePropName}\`,` + + ` please use \`option.initialValue\` instead.` + ) + } + fieldMeta.originalProps = originalProps + fieldMeta.ref = fieldElem.ref + return cloneElement(fieldElem, { + ...props, + ...this.fieldsStore.getFieldValuePropValue(fieldMeta), + }) + } + }, + + getFieldProps (name, usersFieldOption = {}) { + if (!name) { + throw new Error('Must call `getFieldProps` with valid name string!') + } + if (process.env.NODE_ENV !== 'production') { + warning( + this.fieldsStore.isValidNestedFieldName(name), + 'One field name cannot be part of another, e.g. `a` and `a.b`.' + ) + warning( + !('exclusive' in usersFieldOption), + '`option.exclusive` of `getFieldProps`|`getFieldDecorator` had been remove.' + ) + } + + delete this.clearedFieldMetaCache[name] + + const fieldOption = { + name, + trigger: DEFAULT_TRIGGER, + valuePropName: 'value', + validate: [], + ...usersFieldOption, + } + + const { + rules, + trigger, + validateTrigger = trigger, + validate, + } = fieldOption + + const fieldMeta = this.fieldsStore.getFieldMeta(name) + if ('initialValue' in fieldOption) { + fieldMeta.initialValue = fieldOption.initialValue + } + + const inputProps = { + ...this.fieldsStore.getFieldValuePropValue(fieldOption), + ref: this.getCacheBind(name, `${name}__ref`, this.saveRef), + } + if (fieldNameProp) { + inputProps[fieldNameProp] = name + } + + const validateRules = normalizeValidateRules(validate, rules, validateTrigger) + const validateTriggers = getValidateTriggers(validateRules) + validateTriggers.forEach((action) => { + if (inputProps[action]) return + inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate) + }) + + // make sure that the value will be collect + if (trigger && validateTriggers.indexOf(trigger) === -1) { + inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect) + } + + const meta = { + ...fieldMeta, + ...fieldOption, + validate: validateRules, + } + this.fieldsStore.setFieldMeta(name, meta) + if (fieldMetaProp) { + inputProps[fieldMetaProp] = meta + } + + if (fieldDataProp) { + inputProps[fieldDataProp] = this.fieldsStore.getField(name) + } + + return inputProps + }, + + getFieldInstance (name) { + return this.instances[name] + }, + + getRules (fieldMeta, action) { + const actionRules = fieldMeta.validate.filter((item) => { + return !action || item.trigger.indexOf(action) >= 0 + }).map((item) => item.rules) + return flattenArray(actionRules) + }, + + setFields (maybeNestedFields, callback) { + const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields) + this.fieldsStore.setFields(fields) + if (onFieldsChange) { + const changedFields = Object.keys(fields) + .reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {}) + onFieldsChange(this.props, changedFields, this.fieldsStore.getNestedAllFields()) + } + this.forceUpdate(callback) + }, + + resetFields (ns) { + const newFields = this.fieldsStore.resetFields(ns) + if (Object.keys(newFields).length > 0) { + this.setFields(newFields) + } + if (ns) { + const names = Array.isArray(ns) ? ns : [ns] + names.forEach(name => delete this.clearedFieldMetaCache[name]) + } else { + this.clearedFieldMetaCache = {} + } + }, + + setFieldsValue (changedValues, callback) { + const { fieldsMeta } = this.fieldsStore + const values = this.fieldsStore.flattenRegisteredFields(changedValues) + const newFields = Object.keys(values).reduce((acc, name) => { + const isRegistered = fieldsMeta[name] + if (process.env.NODE_ENV !== 'production') { + warning( + isRegistered, + 'Cannot use `setFieldsValue` until ' + + 'you use `getFieldDecorator` or `getFieldProps` to register it.' + ) + } + if (isRegistered) { + const value = values[name] + acc[name] = { + value, + } + } + return acc + }, {}) + this.setFields(newFields, callback) + if (onValuesChange) { + const allValues = this.fieldsStore.getAllValues() + onValuesChange(this.props, changedValues, allValues) + } + }, + + saveRef (name, _, component) { + if (!component) { + // after destroy, delete data + this.clearedFieldMetaCache[name] = { + field: this.fieldsStore.getField(name), + meta: this.fieldsStore.getFieldMeta(name), + } + this.fieldsStore.clearField(name) + delete this.instances[name] + delete this.cachedBind[name] + return + } + this.recoverClearedField(name) + const fieldMeta = this.fieldsStore.getFieldMeta(name) + if (fieldMeta) { + const ref = fieldMeta.ref + if (ref) { + if (typeof ref === 'string') { + throw new Error(`can not set ref string for ${name}`) + } + ref(component) + } + } + this.instances[name] = component + }, + + validateFieldsInternal (fields, { + fieldNames, + action, + options = {}, + }, callback) { + const allRules = {} + const allValues = {} + const allFields = {} + const alreadyErrors = {} + fields.forEach((field) => { + const name = field.name + if (options.force !== true && field.dirty === false) { + if (field.errors) { + set(alreadyErrors, name, { errors: field.errors }) + } + return + } + const fieldMeta = this.fieldsStore.getFieldMeta(name) + const newField = { + ...field, + } + newField.errors = undefined + newField.validating = true + newField.dirty = true + allRules[name] = this.getRules(fieldMeta, action) + allValues[name] = newField.value + allFields[name] = newField + }) + this.setFields(allFields) + // in case normalize + Object.keys(allValues).forEach((f) => { + allValues[f] = this.fieldsStore.getFieldValue(f) + }) + if (callback && isEmptyObject(allFields)) { + callback(isEmptyObject(alreadyErrors) ? null : alreadyErrors, + this.fieldsStore.getFieldsValue(fieldNames)) + return + } + const validator = new AsyncValidator(allRules) + if (validateMessages) { + validator.messages(validateMessages) + } + validator.validate(allValues, options, (errors) => { + const errorsGroup = { + ...alreadyErrors, + } + if (errors && errors.length) { + errors.forEach((e) => { + const fieldName = e.field + const field = get(errorsGroup, fieldName) + if (typeof field !== 'object' || Array.isArray(field)) { + set(errorsGroup, fieldName, { errors: [] }) + } + const fieldErrors = get(errorsGroup, fieldName.concat('.errors')) + fieldErrors.push(e) + }) + } + const expired = [] + const nowAllFields = {} + Object.keys(allRules).forEach((name) => { + const fieldErrors = get(errorsGroup, name) + const nowField = this.fieldsStore.getField(name) + // avoid concurrency problems + if (nowField.value !== allValues[name]) { + expired.push({ + name, + }) + } else { + nowField.errors = fieldErrors && fieldErrors.errors + nowField.value = allValues[name] + nowField.validating = false + nowField.dirty = false + nowAllFields[name] = nowField + } + }) + this.setFields(nowAllFields) + if (callback) { + if (expired.length) { + expired.forEach(({ name }) => { + const fieldErrors = [{ + message: `${name} need to revalidate`, + field: name, + }] + set(errorsGroup, name, { + expired: true, + errors: fieldErrors, + }) + }) + } + + callback(isEmptyObject(errorsGroup) ? null : errorsGroup, + this.fieldsStore.getFieldsValue(fieldNames)) + } + }) + }, + + validateFields (ns, opt, cb) { + const { names, callback, options } = getParams(ns, opt, cb) + const fieldNames = names + ? this.fieldsStore.getValidFieldsFullName(names) + : this.fieldsStore.getValidFieldsName() + const fields = fieldNames + .filter(name => { + const fieldMeta = this.fieldsStore.getFieldMeta(name) + return hasRules(fieldMeta.validate) + }).map((name) => { + const field = this.fieldsStore.getField(name) + field.value = this.fieldsStore.getFieldValue(name) + return field + }) + if (!fields.length) { + if (callback) { + callback(null, this.fieldsStore.getFieldsValue(fieldNames)) + } + return + } + if (!('firstFields' in options)) { + options.firstFields = fieldNames.filter((name) => { + const fieldMeta = this.fieldsStore.getFieldMeta(name) + return !!fieldMeta.validateFirst + }) + } + this.validateFieldsInternal(fields, { + fieldNames, + options, + }, callback) + }, + + isSubmitting () { + if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { + warning( + false, + '`isSubmitting` is deprecated. ' + + 'Actually, it\'s more convenient to handle submitting status by yourself.' + ) + } + return this.state.submitting + }, + + submit (callback) { + if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { + warning( + false, + '`submit` is deprecated.' + + 'Actually, it\'s more convenient to handle submitting status by yourself.' + ) + } + const fn = () => { + this.setState({ + submitting: false, + }) + } + this.setState({ + submitting: true, + }) + callback(fn) + }, + + render () { + const { wrappedComponentRef, ...restProps } = this.props + const formProps = { + [formPropName]: this.getForm(), + } + if (withRef) { + if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { + warning( + false, + '`withRef` is deprecated, please use `wrappedComponentRef` instead. ' + + 'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140' + ) + } + formProps.ref = 'wrappedComponent' + } else if (wrappedComponentRef) { + formProps.ref = wrappedComponentRef + } + const props = mapProps.call(this, { + ...formProps, + ...restProps, + }) + return + }, + }) + + return argumentContainer(Form, WrappedComponent) + } +} + +export default createBaseForm diff --git a/components/vc-form/src/createDOMForm.jsx b/components/vc-form/src/createDOMForm.jsx new file mode 100644 index 000000000..90e514ab8 --- /dev/null +++ b/components/vc-form/src/createDOMForm.jsx @@ -0,0 +1,104 @@ +import scrollIntoView from 'dom-scroll-into-view' +import has from 'lodash/has' +import createBaseForm from './createBaseForm' +import { mixin as formMixin } from './createForm' +import { getParams } from './utils' + +function computedStyle (el, prop) { + const getComputedStyle = window.getComputedStyle + const style = + // If we have getComputedStyle + getComputedStyle + // Query it + // TODO: From CSS-Query notes, we might need (node, null) for FF + ? getComputedStyle(el) + + // Otherwise, we are in IE and use currentStyle + : el.currentStyle + if (style) { + return style[ + // Switch to camelCase for CSSOM + // DEV: Grabbed from jQuery + // https://github.com/jquery/jquery/blob/1.9-stable/src/css.js#L191-L194 + // https://github.com/jquery/jquery/blob/1.9-stable/src/core.js#L593-L597 + prop.replace(/-(\w)/gi, (word, letter) => { + return letter.toUpperCase() + }) + ] + } + return undefined +} + +function getScrollableContainer (n) { + let node = n + let nodeName + /* eslint no-cond-assign:0 */ + while ((nodeName = node.nodeName.toLowerCase()) !== 'body') { + const overflowY = computedStyle(node, 'overflowY') + // https://stackoverflow.com/a/36900407/3040605 + if ( + node !== n && + (overflowY === 'auto' || overflowY === 'scroll') && + node.scrollHeight > node.clientHeight + ) { + return node + } + node = node.parentNode + } + return nodeName === 'body' ? node.ownerDocument : node +} + +const mixin = { + getForm () { + return { + ...formMixin.getForm.call(this), + validateFieldsAndScroll: this.validateFieldsAndScroll, + } + }, + + validateFieldsAndScroll (ns, opt, cb) { + const { names, callback, options } = getParams(ns, opt, cb) + + const newCb = (error, values) => { + if (error) { + const validNames = this.fieldsStore.getValidFieldsName() + let firstNode + let firstTop + for (const name of validNames) { + if (has(error, name)) { + const instance = this.getFieldInstance(name) + if (instance) { + const node = instance.$el + const top = node.getBoundingClientRect().top + if (firstTop === undefined || firstTop > top) { + firstTop = top + firstNode = node + } + } + } + } + if (firstNode) { + const c = options.container || getScrollableContainer(firstNode) + scrollIntoView(firstNode, c, { + onlyScrollIfNeeded: true, + ...options.scroll, + }) + } + } + + if (typeof callback === 'function') { + callback(error, values) + } + } + + return this.validateFields(names, options, newCb) + }, +} + +function createDOMForm (option) { + return createBaseForm({ + ...option, + }, [mixin]) +} + +export default createDOMForm diff --git a/components/vc-form/src/createFieldsStore.jsx b/components/vc-form/src/createFieldsStore.jsx new file mode 100644 index 000000000..0744bfc8d --- /dev/null +++ b/components/vc-form/src/createFieldsStore.jsx @@ -0,0 +1,261 @@ +import set from 'lodash/set' +import createFormField, { isFormField } from './createFormField' +import { + flattenFields, + getErrorStrs, + startsWith, +} from './utils' + +function partOf (a, b) { + return b.indexOf(a) === 0 && ['.', '['].indexOf(b[a.length]) !== -1 +} + +class FieldsStore { + constructor (fields) { + this.fields = this.flattenFields(fields) + this.fieldsMeta = {} + } + + updateFields (fields) { + this.fields = this.flattenFields(fields) + } + + flattenFields (fields) { + return flattenFields( + fields, + (_, node) => isFormField(node), + 'You must wrap field data with `createFormField`.' + ) + } + + flattenRegisteredFields (fields) { + const validFieldsName = this.getAllFieldsName() + return flattenFields( + fields, + path => validFieldsName.indexOf(path) >= 0, + 'You cannot set field before registering it.' + ) + } + + setFieldsInitialValue = (initialValues) => { + const flattenedInitialValues = this.flattenRegisteredFields(initialValues) + const fieldsMeta = this.fieldsMeta + Object.keys(flattenedInitialValues).forEach(name => { + if (fieldsMeta[name]) { + this.setFieldMeta(name, { + ...this.getFieldMeta(name), + initialValue: flattenedInitialValues[name], + }) + } + }) + } + + setFields (fields) { + const fieldsMeta = this.fieldsMeta + const nowFields = { + ...this.fields, + ...fields, + } + const nowValues = {} + Object.keys(fieldsMeta) + .forEach((f) => { nowValues[f] = this.getValueFromFields(f, nowFields) }) + Object.keys(nowValues).forEach((f) => { + const value = nowValues[f] + const fieldMeta = this.getFieldMeta(f) + if (fieldMeta && fieldMeta.normalize) { + const nowValue = + fieldMeta.normalize(value, this.getValueFromFields(f, this.fields), nowValues) + if (nowValue !== value) { + nowFields[f] = { + ...nowFields[f], + value: nowValue, + } + } + } + }) + this.fields = nowFields + } + + resetFields (ns) { + const { fields } = this + const names = ns + ? this.getValidFieldsFullName(ns) + : this.getAllFieldsName() + return names.reduce((acc, name) => { + const field = fields[name] + if (field && 'value' in field) { + acc[name] = {} + } + return acc + }, {}) + } + + setFieldMeta (name, meta) { + this.fieldsMeta[name] = meta + } + + getFieldMeta (name) { + this.fieldsMeta[name] = this.fieldsMeta[name] || {} + return this.fieldsMeta[name] + } + + getValueFromFields (name, fields) { + const field = fields[name] + if (field && 'value' in field) { + return field.value + } + const fieldMeta = this.getFieldMeta(name) + return fieldMeta && fieldMeta.initialValue + } + + getAllValues = () => { + const { fieldsMeta, fields } = this + return Object.keys(fieldsMeta) + .reduce((acc, name) => set(acc, name, this.getValueFromFields(name, fields)), {}) + } + + getValidFieldsName () { + const { fieldsMeta } = this + return fieldsMeta + ? Object.keys(fieldsMeta).filter(name => !this.getFieldMeta(name).hidden) + : [] + } + + getAllFieldsName () { + const { fieldsMeta } = this + return fieldsMeta ? Object.keys(fieldsMeta) : [] + } + + getValidFieldsFullName (maybePartialName) { + const maybePartialNames = Array.isArray(maybePartialName) + ? maybePartialName : [maybePartialName] + return this.getValidFieldsName() + .filter(fullName => maybePartialNames.some(partialName => ( + fullName === partialName || ( + startsWith(fullName, partialName) && + ['.', '['].indexOf(fullName[partialName.length]) >= 0 + ) + ))) + } + + getFieldValuePropValue (fieldMeta) { + const { name, getValueProps, valuePropName } = fieldMeta + const field = this.getField(name) + const fieldValue = 'value' in field + ? field.value : fieldMeta.initialValue + if (getValueProps) { + return getValueProps(fieldValue) + } + return { [valuePropName]: fieldValue } + } + + getField (name) { + return { + ...this.fields[name], + name, + } + } + + getNotCollectedFields () { + return this.getValidFieldsName() + .filter(name => !this.fields[name]) + .map(name => ({ + name, + dirty: false, + value: this.getFieldMeta(name).initialValue, + })) + .reduce((acc, field) => set(acc, field.name, createFormField(field)), {}) + } + + getNestedAllFields () { + return Object.keys(this.fields) + .reduce( + (acc, name) => set(acc, name, createFormField(this.fields[name])), + this.getNotCollectedFields() + ) + } + + getFieldMember (name, member) { + return this.getField(name)[member] + } + + getNestedFields (names, getter) { + const fields = names || this.getValidFieldsName() + return fields.reduce((acc, f) => set(acc, f, getter(f)), {}) + } + + getNestedField (name, getter) { + const fullNames = this.getValidFieldsFullName(name) + if ( + fullNames.length === 0 || // Not registered + (fullNames.length === 1 && fullNames[0] === name) // Name already is full name. + ) { + return getter(name) + } + const isArrayValue = fullNames[0][name.length] === '[' + const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1 + return fullNames + .reduce( + (acc, fullName) => set( + acc, + fullName.slice(suffixNameStartIndex), + getter(fullName) + ), + isArrayValue ? [] : {} + ) + } + + getFieldsValue = (names) => { + return this.getNestedFields(names, this.getFieldValue) + } + + getFieldValue = (name) => { + const { fields } = this + return this.getNestedField(name, (fullName) => this.getValueFromFields(fullName, fields)) + } + + getFieldsError = (names) => { + return this.getNestedFields(names, this.getFieldError) + } + + getFieldError = (name) => { + return this.getNestedField( + name, + (fullName) => getErrorStrs(this.getFieldMember(fullName, 'errors')) + ) + } + + isFieldValidating = (name) => { + return this.getFieldMember(name, 'validating') + } + + isFieldsValidating = (ns) => { + const names = ns || this.getValidFieldsName() + return names.some((n) => this.isFieldValidating(n)) + } + + isFieldTouched = (name) => { + return this.getFieldMember(name, 'touched') + } + + isFieldsTouched = (ns) => { + const names = ns || this.getValidFieldsName() + return names.some((n) => this.isFieldTouched(n)) + } + + // @private + // BG: `a` and `a.b` cannot be use in the same form + isValidNestedFieldName (name) { + const names = this.getAllFieldsName() + return names.every(n => !partOf(n, name) && !partOf(name, n)) + } + + clearField (name) { + delete this.fields[name] + delete this.fieldsMeta[name] + } +} + +export default function createFieldsStore (fields) { + return new FieldsStore(fields) +} diff --git a/components/vc-form/src/createForm.jsx b/components/vc-form/src/createForm.jsx new file mode 100644 index 000000000..4667bb451 --- /dev/null +++ b/components/vc-form/src/createForm.jsx @@ -0,0 +1,32 @@ +import createBaseForm from './createBaseForm' + +export const mixin = { + getForm () { + return { + getFieldsValue: this.fieldsStore.getFieldsValue, + getFieldValue: this.fieldsStore.getFieldValue, + getFieldInstance: this.getFieldInstance, + setFieldsValue: this.setFieldsValue, + setFields: this.setFields, + setFieldsInitialValue: this.fieldsStore.setFieldsInitialValue, + getFieldDecorator: this.getFieldDecorator, + getFieldProps: this.getFieldProps, + getFieldsError: this.fieldsStore.getFieldsError, + getFieldError: this.fieldsStore.getFieldError, + isFieldValidating: this.fieldsStore.isFieldValidating, + isFieldsValidating: this.fieldsStore.isFieldsValidating, + isFieldsTouched: this.fieldsStore.isFieldsTouched, + isFieldTouched: this.fieldsStore.isFieldTouched, + isSubmitting: this.isSubmitting, + submit: this.submit, + validateFields: this.validateFields, + resetFields: this.resetFields, + } + }, +} + +function createForm (options) { + return createBaseForm(options, [mixin]) +} + +export default createForm diff --git a/components/vc-form/src/createFormField.jsx b/components/vc-form/src/createFormField.jsx new file mode 100644 index 000000000..9843449ac --- /dev/null +++ b/components/vc-form/src/createFormField.jsx @@ -0,0 +1,16 @@ +class Field { + constructor (fields) { + Object.assign(this, fields) + } +} + +export function isFormField (obj) { + return obj instanceof Field +} + +export default function createFormField (field) { + if (isFormField(field)) { + return field + } + return new Field(field) +} diff --git a/components/vc-form/src/index.jsx b/components/vc-form/src/index.jsx new file mode 100644 index 000000000..ad1f4f338 --- /dev/null +++ b/components/vc-form/src/index.jsx @@ -0,0 +1,6 @@ +// export this package's api +import createForm from './createForm' +import createFormField from './createFormField' +import formShape from './propTypes' + +export { createForm, createFormField, formShape } diff --git a/components/vc-form/src/propTypes.js b/components/vc-form/src/propTypes.js new file mode 100644 index 000000000..70cdf28dd --- /dev/null +++ b/components/vc-form/src/propTypes.js @@ -0,0 +1,24 @@ +import PropTypes from '../../_util/vue-types' + +const formShape = PropTypes.shape({ + getFieldsValue: PropTypes.func, + getFieldValue: PropTypes.func, + getFieldInstance: PropTypes.func, + setFieldsValue: PropTypes.func, + setFields: PropTypes.func, + setFieldsInitialValue: PropTypes.func, + getFieldDecorator: PropTypes.func, + getFieldProps: PropTypes.func, + getFieldsError: PropTypes.func, + getFieldError: PropTypes.func, + isFieldValidating: PropTypes.func, + isFieldsValidating: PropTypes.func, + isFieldsTouched: PropTypes.func, + isFieldTouched: PropTypes.func, + isSubmitting: PropTypes.func, + submit: PropTypes.func, + validateFields: PropTypes.func, + resetFields: PropTypes.func, +}).loose + +export default formShape diff --git a/components/vc-form/src/utils.js b/components/vc-form/src/utils.js new file mode 100644 index 000000000..ca3ef27b5 --- /dev/null +++ b/components/vc-form/src/utils.js @@ -0,0 +1,153 @@ +import hoistStatics from 'hoist-non-react-statics' + +function getDisplayName (WrappedComponent) { + return WrappedComponent.displayName || WrappedComponent.name || 'WrappedComponent' +} + +export function argumentContainer (Container, WrappedComponent) { + /* eslint no-param-reassign:0 */ + Container.displayName = `Form(${getDisplayName(WrappedComponent)})` + Container.WrappedComponent = WrappedComponent + return hoistStatics(Container, WrappedComponent) +} + +export function identity (obj) { + return obj +} + +export function flattenArray (arr) { + return Array.prototype.concat.apply([], arr) +} + +export function treeTraverse (path = '', tree, isLeafNode, errorMessage, callback) { + if (isLeafNode(path, tree)) { + callback(path, tree) + } else if (tree === undefined) { + return + } else if (Array.isArray(tree)) { + tree.forEach((subTree, index) => treeTraverse( + `${path}[${index}]`, + subTree, + isLeafNode, + errorMessage, + callback + )) + } else { // It's object and not a leaf node + if (typeof tree !== 'object') { + console.error(errorMessage) + return + } + Object.keys(tree).forEach(subTreeKey => { + const subTree = tree[subTreeKey] + treeTraverse( + `${path}${path ? '.' : ''}${subTreeKey}`, + subTree, + isLeafNode, + errorMessage, + callback + ) + }) + } +} + +export function flattenFields (maybeNestedFields, isLeafNode, errorMessage) { + const fields = {} + treeTraverse(undefined, maybeNestedFields, isLeafNode, errorMessage, (path, node) => { + fields[path] = node + }) + return fields +} + +export function normalizeValidateRules (validate, rules, validateTrigger) { + const validateRules = validate.map((item) => { + const newItem = { + ...item, + trigger: item.trigger || [], + } + if (typeof newItem.trigger === 'string') { + newItem.trigger = [newItem.trigger] + } + return newItem + }) + if (rules) { + validateRules.push({ + trigger: validateTrigger ? [].concat(validateTrigger) : [], + rules, + }) + } + return validateRules +} + +export function getValidateTriggers (validateRules) { + return validateRules + .filter(item => !!item.rules && item.rules.length) + .map(item => item.trigger) + .reduce((pre, curr) => pre.concat(curr), []) +} + +export function getValueFromEvent (e) { + // To support custom element + if (!e || !e.target) { + return e + } + const { target } = e + return target.type === 'checkbox' ? target.checked : target.value +} + +export function getErrorStrs (errors) { + if (errors) { + return errors.map((e) => { + if (e && e.message) { + return e.message + } + return e + }) + } + return errors +} + +export function getParams (ns, opt, cb) { + let names = ns + let options = opt + let callback = cb + if (cb === undefined) { + if (typeof names === 'function') { + callback = names + options = {} + names = undefined + } else if (Array.isArray(names)) { + if (typeof options === 'function') { + callback = options + options = {} + } else { + options = options || {} + } + } else { + callback = options + options = names || {} + names = undefined + } + } + return { + names, + options, + callback, + } +} + +export function isEmptyObject (obj) { + return Object.keys(obj).length === 0 +} + +export function hasRules (validate) { + if (validate) { + return validate.some((item) => { + return item.rules && item.rules.length + }) + } + return false +} + +export function startsWith (str, prefix) { + return str.lastIndexOf(prefix, 0) === 0 +}