From 3694ad80c78042099d1249eb1a67f4f53cb80056 Mon Sep 17 00:00:00 2001 From: tjz <415800467@qq.com> Date: Tue, 24 Apr 2018 22:52:16 +0800 Subject: [PATCH 01/39] fix: menu click bug --- components/menu/index.jsx | 3 +++ site/routes.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/components/menu/index.jsx b/components/menu/index.jsx index 3082862f2..4b316619d 100644 --- a/components/menu/index.jsx +++ b/components/menu/index.jsx @@ -231,6 +231,9 @@ export default { menuProps.on.click = this.handleClick menuProps.props.openTransitionName = menuOpenAnimation } else { + menuProps.on.click = (e) => { + this.$emit('click', e) + } menuProps.props.openAnimation = menuOpenAnimation } diff --git a/site/routes.js b/site/routes.js index 01ce5bd01..6aa0a45f2 100644 --- a/site/routes.js +++ b/site/routes.js @@ -4,7 +4,7 @@ import Iframe from './components/iframe.vue' const AsyncTestComp = () => { const d = window.location.hash.replace('#', '') return { - component: import(`../components/layout/demo/${d}`), + component: import(`../components/menu/demo/${d}`), } } From 4221b0b9aa64615113e1ed4bd43d4a51bf216561 Mon Sep 17 00:00:00 2001 From: tjz <415800467@qq.com> Date: Tue, 24 Apr 2018 22:56:57 +0800 Subject: [PATCH 02/39] docs: add 0.4.2 changelog --- CHANGELOG.en-US.md | 6 ++++++ CHANGELOG.zh-CN.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index ab4516d4f..d03f2bc22 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -2,6 +2,12 @@ --- +## 0.4.2 + +`2018-04-24` + +- 🐞 fix menu click bug + ## 0.4.1 #### bug diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 7cc218217..5028a04dd 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -2,6 +2,12 @@ --- +## 0.4.2 + +`2018-04-24` + +- 🐞 修复menu 非 inline 模式下的 click bug + ## 0.4.1 #### bug From e71bb0375d82b2a6eebd6f6f9656578c4a5f8fa1 Mon Sep 17 00:00:00 2001 From: tjz <415800467@qq.com> Date: Tue, 24 Apr 2018 22:59:02 +0800 Subject: [PATCH 03/39] bump 0.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b79573b8..e8f851c29 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-antd-ui", - "version": "0.4.1", + "version": "0.4.2", "title": "Ant Design Vue", "description": "An enterprise-class UI design language and Vue-based implementation", "keywords": [ From 104c7134dd7bbea983e4eb21312bf6dcc3d0368e Mon Sep 17 00:00:00 2001 From: wangxueliang Date: Wed, 25 Apr 2018 11:30:47 +0800 Subject: [PATCH 04/39] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=90=8E=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/input-number/style/{index.jsx => index.js} | 0 components/timeline/style/{index.jsx => index.js} | 0 components/transfer/style/{index.jsx => index.js} | 0 components/upload/style/{index.jsx => index.js} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename components/input-number/style/{index.jsx => index.js} (100%) rename components/timeline/style/{index.jsx => index.js} (100%) rename components/transfer/style/{index.jsx => index.js} (100%) rename components/upload/style/{index.jsx => index.js} (100%) diff --git a/components/input-number/style/index.jsx b/components/input-number/style/index.js similarity index 100% rename from components/input-number/style/index.jsx rename to components/input-number/style/index.js diff --git a/components/timeline/style/index.jsx b/components/timeline/style/index.js similarity index 100% rename from components/timeline/style/index.jsx rename to components/timeline/style/index.js diff --git a/components/transfer/style/index.jsx b/components/transfer/style/index.js similarity index 100% rename from components/transfer/style/index.jsx rename to components/transfer/style/index.js diff --git a/components/upload/style/index.jsx b/components/upload/style/index.js similarity index 100% rename from components/upload/style/index.jsx rename to components/upload/style/index.js From 16f6a644bb968b1012eef38fa33a54474dc1943b Mon Sep 17 00:00:00 2001 From: lokyoung Date: Fri, 27 Apr 2018 14:06:26 +0800 Subject: [PATCH 05/39] fix: remove vscode config --- .gitignore | 1 + .vscode/settings.json | 42 ------------------------------------------ 2 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 9c887521b..ee76b414f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ typings/ .yarn-integrity # dotenv environment variables file +.vscode .env .idea .DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 8090fa64a..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,42 +0,0 @@ -// 将设置放入此文件中以覆盖默认值和用户设置。 -{ - "eslint.enable": true, - "eslint.options": { - "extensions": [ - ".js", - ".jsx", - ".vue" - ], - "configFile": ".eslintrc" - }, - "eslint.validate": [ - { - "language": "html", - "autoFix": true - }, - { - "language": "vue", - "autoFix": true - }, - { - "language": "javascript", - "autoFix": true - }, - { - "language": "javascriptreact", - "autoFix": true - } - ], - "emmet.syntaxProfiles": { - "vue-html": "html", - "vue": "html" - }, - "eslint.autoFixOnSave": true, - "vetur.validation.template": true, - "vetur.format.html.wrap_line_length": 60, - "vetur.format.js.InsertSpaceBeforeFunctionParenthesis": true, - "stylefmt.config": { - "fix": true - }, - "editor.tabSize": 2 -} From 9368c0886c053ee18929470d55c9902fa0d95322 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 2 May 2018 09:59:42 +0800 Subject: [PATCH 06/39] docs: add site babel-polyfill --- site/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/site/index.js b/site/index.js index 572938ff6..1aa3622aa 100644 --- a/site/index.js +++ b/site/index.js @@ -1,3 +1,4 @@ +import 'babel-polyfill' import '../components/style.js' import './index.less' import 'highlight.js/styles/solarized-light.css' From 6a9072d17f2030467cb6d24a82276f523fbf1539 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 2 May 2018 10:13:46 +0800 Subject: [PATCH 07/39] docs: add 0.4.3 changelog --- CHANGELOG.en-US.md | 7 +++++++ CHANGELOG.zh-CN.md | 7 +++++++ antd-tools/gulpfile.js | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index d03f2bc22..4106c8c29 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -2,6 +2,13 @@ --- +## 0.4.3 + +`2018-05-02` + +- 🐞 Fix component style loss problem +- 🌟 site add babel-polyfill + ## 0.4.2 `2018-04-24` diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 5028a04dd..9114422fc 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -2,6 +2,13 @@ --- +## 0.4.3 + +`2018-05-02` + +- 🐞 修复组件样式丢失问题 +- 🌟 站点添加babel-polyfill + ## 0.4.2 `2018-04-24` diff --git a/antd-tools/gulpfile.js b/antd-tools/gulpfile.js index c340cbddf..4baef31f9 100644 --- a/antd-tools/gulpfile.js +++ b/antd-tools/gulpfile.js @@ -78,12 +78,12 @@ function babelify (js, modules) { let stream = js.pipe(babel(babelConfig)) .pipe(through2.obj(function z (file, encoding, next) { this.push(file.clone()) - if (file.path.match(/\/style\/index\.js/)) { + if (file.path.match(/\/style\/index\.(js|jsx)$/)) { const content = file.contents.toString(encoding) file.contents = Buffer.from(content .replace(/\/style\/?'/g, '/style/css\'') .replace(/\.less/g, '.css')) - file.path = file.path.replace(/index\.js/, 'css.js') + file.path = file.path.replace(/index\.(js|jsx)$/, 'css.js') this.push(file) next() } else { From fb5c66d007098d5ffbcee8d7ed9f90e6d73b8b49 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 2 May 2018 10:15:08 +0800 Subject: [PATCH 08/39] bump 0.4.3 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e8f851c29..f1d73f7f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-antd-ui", - "version": "0.4.2", + "version": "0.4.3", "title": "Ant Design Vue", "description": "An enterprise-class UI design language and Vue-based implementation", "keywords": [ @@ -161,4 +161,4 @@ "shallowequal": "^1.0.2", "warning": "^3.0.0" } -} \ No newline at end of file +} From fca425f7205e95c741c28e171687ca7a73493c98 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 2 May 2018 13:31:58 +0800 Subject: [PATCH 09/39] fix: build scripts gulp task bug --- antd-tools/gulpfile.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/antd-tools/gulpfile.js b/antd-tools/gulpfile.js index 4baef31f9..1e06a536b 100644 --- a/antd-tools/gulpfile.js +++ b/antd-tools/gulpfile.js @@ -238,11 +238,17 @@ function pub (done) { gulp.task('dist', ['compile'], (done) => { dist(done) }) -gulp.task('compile', ['compile-with-es'], () => { +gulp.task('compile', ['compile-with-es'], (done) => { compile() + .on('finish', function () { + done() + }) }) -gulp.task('compile-with-es', () => { +gulp.task('compile-with-es', (done) => { compile(false) + .on('finish', function () { + done() + }) }) gulp.task('pub', ['check-git', 'compile'], (done) => { From c04144afd94e87b82775caf9213db2d5c571eda3 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 2 May 2018 16:47:36 +0800 Subject: [PATCH 10/39] =?UTF-8?q?docs:=20add=20=E5=AE=98=E7=BD=91=E5=9B=BD?= =?UTF-8?q?=E5=86=85=E9=95=9C=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-zh_CN.md | 1 + README.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/README-zh_CN.md b/README-zh_CN.md index f461570c2..2fead4df9 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -13,6 +13,7 @@ Ant Design 3.X 的 Vue 实现,开发和服务于企业级后台产品。 +[官网国内镜像](http://tangjinzhou.gitee.io/ant-design/docs/vue/introduce-cn/) [README in English](README.md) diff --git a/README.md b/README.md index fad50ae1e..ebdf2d1f7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ An enterprise-class UI components based on Ant Design 3.X and Vue. +[官网国内镜像](http://tangjinzhou.gitee.io/ant-design/docs/vue/introduce-cn/) + [中文 README](README-zh_CN.md) ## Features 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 11/39] 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 +} From 07b0d46864ee225577e500791b690212ba5bce63 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Thu, 3 May 2018 19:10:43 +0800 Subject: [PATCH 12/39] feat: update vc-form --- antd-tools/getBabelCommonConfig.js | 1 + components/vc-form/demo/input-array.jsx | 66 ++ components/vc-form/demo/styles.js | 11 + components/vc-form/src/createBaseForm.jsx | 868 +++++++++++----------- components/vc-form/src/createDOMForm.jsx | 71 +- components/vc-form/src/createForm.jsx | 44 +- components/vc-form/src/utils.js | 9 +- package.json | 325 ++++---- site/routes.js | 2 +- 9 files changed, 743 insertions(+), 654 deletions(-) create mode 100644 components/vc-form/demo/input-array.jsx create mode 100644 components/vc-form/demo/styles.js diff --git a/antd-tools/getBabelCommonConfig.js b/antd-tools/getBabelCommonConfig.js index b65885420..dec16d11b 100644 --- a/antd-tools/getBabelCommonConfig.js +++ b/antd-tools/getBabelCommonConfig.js @@ -7,6 +7,7 @@ module.exports = function (modules) { require.resolve('babel-plugin-transform-es3-property-literals'), require.resolve('babel-plugin-transform-object-assign'), require.resolve('babel-plugin-transform-object-rest-spread'), + require.resolve('babel-plugin-transform-class-properties'), ] plugins.push([require.resolve('babel-plugin-transform-runtime'), { polyfill: false, diff --git a/components/vc-form/demo/input-array.jsx b/components/vc-form/demo/input-array.jsx new file mode 100644 index 000000000..7a66e6c2c --- /dev/null +++ b/components/vc-form/demo/input-array.jsx @@ -0,0 +1,66 @@ +/* eslint no-console:0 */ + +import { createForm } from '../index' +import { regionStyle } from './styles' + +let uuid = 0 + +const Form = { + props: { + form: Object, + }, + methods: { + remove (k) { + const { form } = this + // can use data-binding to get + let keys = form.getFieldValue('keys') + keys = keys.filter((key) => { + return key !== k + }) + // can use data-binding to set + form.setFieldsValue({ + keys, + }) + }, + add () { + uuid++ + const { form } = this + // can use data-binding to get + let keys = form.getFieldValue('keys') + keys = keys.concat(uuid) + // can use data-binding to set + // important! notify form to detect changes + form.setFieldsValue({ + keys, + }) + }, + submit (e) { + e.preventDefault() + console.log(this.form.getFieldsValue()) + }, + }, + + render () { + const { getFieldProps, getFieldValue } = this.form + getFieldProps('keys', { + initialValue: [], + }) + const inputs = getFieldValue('keys').map((k) => { + return (
+ + delete
) + }) + return (
+ {inputs} +
+ + + +
+
) + }, +} + +export default createForm()(Form) diff --git a/components/vc-form/demo/styles.js b/components/vc-form/demo/styles.js new file mode 100644 index 000000000..e0a5fe5ac --- /dev/null +++ b/components/vc-form/demo/styles.js @@ -0,0 +1,11 @@ +export const regionStyle = { + border: '1px solid red', + marginTop: '10px', + padding: '10px', +} + +export const errorStyle = { + color: 'red', + marginTop: '10px', + padding: '10px', +} diff --git a/components/vc-form/src/createBaseForm.jsx b/components/vc-form/src/createBaseForm.jsx index 78a0a4b4a..23c60d38e 100644 --- a/components/vc-form/src/createBaseForm.jsx +++ b/components/vc-form/src/createBaseForm.jsx @@ -1,10 +1,11 @@ -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 BaseMixin from '../../_util/BaseMixin' +import PropTypes from '../../_util/vue-types' import { argumentContainer, identity, @@ -35,11 +36,14 @@ function createBaseForm (option = {}, mixins = []) { } = option return function decorate (WrappedComponent) { - const Form = createReactClass({ - mixins, - - getInitialState () { - const fields = mapPropsToFields && mapPropsToFields(this.props) + const Form = { + mixins: [BaseMixin, ...mixins], + props: { + hideRequiredMark: PropTypes.bool, + layout: PropTypes.string, + }, + data () { + const fields = mapPropsToFields && mapPropsToFields(this.$props) this.fieldsStore = createFieldsStore(fields || {}) this.instances = {} @@ -56,13 +60,6 @@ function createBaseForm (option = {}, mixins = []) { '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) } }) @@ -71,456 +68,465 @@ function createBaseForm (option = {}, mixins = []) { 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, + watch: { + '$props': { + handler: function (nextProps) { + if (mapPropsToFields) { + this.fieldsStore.updateFields(mapPropsToFields(nextProps)) + } }, - }) + deep: true, + }, }, - - 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) => { + methods: { + onCollectCommon (name, action, args) { 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.` - ) + if (fieldMeta[action]) { + fieldMeta[action](...args) + } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) { + fieldMeta.originalProps[action](...args) } - fieldMeta.originalProps = originalProps - fieldMeta.ref = fieldElem.ref - return cloneElement(fieldElem, { - ...props, - ...this.fieldsStore.getFieldValuePropValue(fieldMeta), + 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, }) - } - }, + }, - 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.' - ) - } + 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, + }, + }) + }, - delete this.clearedFieldMetaCache[name] + 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] + }, - const fieldOption = { - name, - trigger: DEFAULT_TRIGGER, - valuePropName: 'value', - validate: [], - ...usersFieldOption, - } + 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] + } + }, - const { - rules, - trigger, - validateTrigger = trigger, - validate, - } = fieldOption + getFieldDecorator (name, fieldOption) { + const { ref, ...restProps } = 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: { + ...restProps, + ...this.fieldsStore.getFieldValuePropValue(fieldMeta), + }, + ref, + }) + } + }, - 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] + getFieldProps (name, usersFieldOption = {}) { + if (!name) { + throw new Error('Must call `getFieldProps` with valid name string!') + } if (process.env.NODE_ENV !== 'production') { warning( - isRegistered, - 'Cannot use `setFieldsValue` until ' + - 'you use `getFieldDecorator` or `getFieldProps` to register it.' + 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.' ) } - if (isRegistered) { - const value = values[name] - acc[name] = { - value, + + 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: name, + } + const saveRef = this.getCacheBind(name, `${name}__ref`, this.saveRef) + this.$nextTick(() => { + if (this.instances[name] !== this.$refs[name]) { + this.$refs[name].destroyed = () => { + this.$refs[name].destroyed() + // 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] + } + } + saveRef(name, `${name}__ref`, this.$refs[name]) + }) + 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) { + 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) } } - return acc - }, {}) - this.setFields(newFields, callback) - if (onValuesChange) { - const allValues = this.fieldsStore.getAllValues() - onValuesChange(this.props, changedValues, allValues) - } - }, + this.instances[name] = component + }, - 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}`) + 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 } - ref(component) + 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 } - } - this.instances[name] = component - }, + 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, + }) + }) + } - 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 }) + 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 } - 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) + if (!('firstFields' in options)) { + options.firstFields = fieldNames.filter((name) => { + const fieldMeta = this.fieldsStore.getFieldMeta(name) + return !!fieldMeta.validateFirst }) } - 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, - }) - }) - } + this.validateFieldsInternal(fields, { + fieldNames, + options, + }, callback) + }, - 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) { + isSubmitting () { 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' + '`isSubmitting` is deprecated. ' + + 'Actually, it\'s more convenient to handle submitting status by yourself.' ) } - formProps.ref = 'wrappedComponent' - } else if (wrappedComponentRef) { - formProps.ref = wrappedComponentRef - } - const props = mapProps.call(this, { - ...formProps, - ...restProps, - }) - return + return this.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 { $props, $listeners } = this + const formProps = { + [formPropName]: this.getForm(), + } + + const wrappedComponentProps = { + props: mapProps.call(this, { + ...formProps, + ...$props, + }), + on: $listeners, + } + if (withRef) { + wrappedComponentProps.ref = 'wrappedComponent' + } + return + }, + } return argumentContainer(Form, WrappedComponent) } diff --git a/components/vc-form/src/createDOMForm.jsx b/components/vc-form/src/createDOMForm.jsx index 90e514ab8..9fdc5dc12 100644 --- a/components/vc-form/src/createDOMForm.jsx +++ b/components/vc-form/src/createDOMForm.jsx @@ -49,50 +49,53 @@ function getScrollableContainer (n) { } const mixin = { - getForm () { - return { - ...formMixin.getForm.call(this), - validateFieldsAndScroll: this.validateFieldsAndScroll, - } - }, + methods: { + getForm () { + return { + ...formMixin.getForm.call(this), + validateFieldsAndScroll: this.validateFieldsAndScroll, + } + }, - validateFieldsAndScroll (ns, opt, cb) { - const { names, callback, options } = getParams(ns, opt, cb) + 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 + 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 (firstNode) { - const c = options.container || getScrollableContainer(firstNode) - scrollIntoView(firstNode, c, { - onlyScrollIfNeeded: true, - ...options.scroll, - }) + + if (typeof callback === 'function') { + callback(error, values) } } - if (typeof callback === 'function') { - callback(error, values) - } - } - - return this.validateFields(names, options, newCb) + return this.validateFields(names, options, newCb) + }, }, + } function createDOMForm (option) { diff --git a/components/vc-form/src/createForm.jsx b/components/vc-form/src/createForm.jsx index 4667bb451..82eb056ec 100644 --- a/components/vc-form/src/createForm.jsx +++ b/components/vc-form/src/createForm.jsx @@ -1,27 +1,29 @@ 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, - } + methods: { + 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, + } + }, }, } diff --git a/components/vc-form/src/utils.js b/components/vc-form/src/utils.js index ca3ef27b5..7819380bc 100644 --- a/components/vc-form/src/utils.js +++ b/components/vc-form/src/utils.js @@ -1,14 +1,13 @@ -import hoistStatics from 'hoist-non-react-statics' - function getDisplayName (WrappedComponent) { - return WrappedComponent.displayName || WrappedComponent.name || 'WrappedComponent' + return WrappedComponent.name || 'WrappedComponent' } export function argumentContainer (Container, WrappedComponent) { /* eslint no-param-reassign:0 */ - Container.displayName = `Form(${getDisplayName(WrappedComponent)})` + Container.name = `Form_${getDisplayName(WrappedComponent)}` Container.WrappedComponent = WrappedComponent - return hoistStatics(Container, WrappedComponent) + Container.methods = { ...Container.methods, ...WrappedComponent.methods } + return Container } export function identity (obj) { diff --git a/package.json b/package.json index f1d73f7f6..404d4bed9 100644 --- a/package.json +++ b/package.json @@ -1,164 +1,165 @@ { - "name": "vue-antd-ui", - "version": "0.4.3", - "title": "Ant Design Vue", - "description": "An enterprise-class UI design language and Vue-based implementation", - "keywords": [ - "ant", - "design", - "antd", - "vue", - "vueComponent", - "component", - "components", - "ui", - "framework", - "frontend" - ], - "main": "dist/antd.min.js", - "files": [ - "dist", - "lib", - "es" - ], - "scripts": { - "start": "NODE_ENV=development ./node_modules/.bin/webpack-dev-server --open --hot", - "test": "karma start test/karma.conf.js --single-run", - "site": "node scripts/run.js site-dist", - "copy": "node scripts/run.js copy-html", - "compile": "node antd-tools/cli/run.js compile", - "pub": "node antd-tools/cli/run.js pub", - "prepublish": "node antd-tools/cli/run.js guard", - "dist": "node antd-tools/cli/run.js dist", - "lint": "eslint -c ./.eslintrc --fix --ext .jsx,.js,.vue ./components", - "lint:style": "stylelint \"./examples/**/*.less\" --fix --syntax less", - "commitmsg": "validate-commit-msg", - "cm": "git-cz" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/vueComponent/ant-design.git" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/vueComponent/ant-design/issues" - }, - "homepage": "https://github.com/vueComponent/ant-design", - "pre-commit": [ - "lint:style", - "lint" - ], - "devDependencies": { - "@octokit/rest": "^15.2.6", - "autoprefixer": "^8.1.0", - "babel-cli": "^6.26.0", - "babel-core": "^6.26.0", - "babel-eslint": "^8.0.1", - "babel-helper-vue-jsx-merge-props": "^2.0.3", - "babel-loader": "^7.1.2", - "babel-plugin-add-module-exports": "^0.2.1", - "babel-plugin-import": "^1.1.1", - "babel-plugin-istanbul": "^4.1.1", - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "babel-plugin-transform-decorators": "^6.24.1", - "babel-plugin-transform-decorators-legacy": "^1.3.4", - "babel-plugin-transform-es3-member-expression-literals": "^6.22.0", - "babel-plugin-transform-es3-property-literals": "^6.22.0", - "babel-plugin-transform-object-assign": "^6.22.0", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-plugin-transform-runtime": "~6.23.0", - "babel-plugin-transform-vue-jsx": "^3.7.0", - "babel-polyfill": "^6.26.0", - "babel-preset-env": "^1.6.1", - "case-sensitive-paths-webpack-plugin": "^2.1.2", - "chai": "^4.1.2", - "chalk": "^2.3.2", - "cheerio": "^1.0.0-rc.2", - "colorful": "^2.1.0", - "commander": "^2.15.0", - "commitizen": "^2.9.6", - "css-loader": "^0.28.7", - "deep-assign": "^2.0.0", - "eslint": "^4.7.2", - "eslint-plugin-vue": "^3.13.0", - "eslint-plugin-html": "^3.2.2", - "eslint-plugin-vue-libs": "^1.2.1", - "extract-text-webpack-plugin": "^3.0.2", - "fetch-jsonp": "^1.1.3", - "gulp": "^3.9.1", - "gulp-babel": "^7.0.0", - "gulp-strip-code": "^0.1.4", - "highlight.js": "^9.12.0", - "html-webpack-plugin": "^2.30.1", - "husky": "^0.14.3", - "istanbul-instrumenter-loader": "^3.0.0", - "jsonp": "^0.2.1", - "karma": "^1.4.1", - "karma-coverage": "^1.1.1", - "karma-coverage-istanbul-reporter": "^1.3.0", - "karma-mocha": "^1.3.0", - "karma-phantomjs-launcher": "^1.0.2", - "karma-phantomjs-shim": "^1.4.0", - "karma-sinon-chai": "^1.3.1", - "karma-sourcemap-loader": "^0.3.7", - "karma-spec-reporter": "0.0.31", - "karma-webpack": "^2.0.2", - "less": "^2.7.2", - "less-loader": "^4.0.5", - "less-plugin-npm-import": "^2.1.0", - "markdown-it": "^8.4.0", - "markdown-it-anchor": "^4.0.0", - "marked": "^0.3.7", - "merge2": "^1.2.1", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "mocha": "^3.2.0", - "postcss": "^6.0.20", - "postcss-loader": "^2.1.2", - "pre-commit": "^1.2.2", - "querystring": "^0.2.0", - "reqwest": "^2.0.5", - "rimraf": "^2.6.2", - "rucksack-css": "^1.0.2", - "selenium-server": "^3.0.1", - "semver": "^5.3.0", - "sinon": "^4.0.2", - "sinon-chai": "^2.8.0", - "style-loader": "^0.18.2", - "stylelint": "^8.1.1", - "stylelint-config-standard": "^17.0.0", - "through2": "^2.0.3", - "validate-commit-msg": "^2.14.0", - "vue": "^2.5.15", - "vue-antd-md-loader": "^1.0.3", - "vue-clipboard2": "0.0.8", - "vue-loader": "^13.0.5", - "vue-router": "^3.0.1", - "vue-template-compiler": "^2.5.15", - "webpack": "^3.6.0", - "webpack-chunk-hash": "^0.5.0", - "webpack-dev-server": "^2.8.2", - "webpack-merge": "^4.1.1" - }, - "dependencies": { - "add-dom-event-listener": "^1.0.2", - "array-tree-filter": "^2.1.0", - "babel-runtime": "6.x", - "babel-helper-vue-jsx-merge-props": "^2.0.3", - "classnames": "^2.2.5", - "component-classes": "^1.2.6", - "css-animation": "^1.4.1", - "dom-align": "^1.6.7", - "dom-closest": "^0.2.0", - "dom-scroll-into-view": "^1.2.1", - "enquire.js": "^2.1.6", - "is-negative-zero": "^2.0.0", - "lodash": "^4.17.5", - "moment": "^2.21.0", - "omit.js": "^1.0.0", - "shallow-equal": "^1.0.0", - "shallowequal": "^1.0.2", - "warning": "^3.0.0" - } + "name": "vue-antd-ui", + "version": "0.4.3", + "title": "Ant Design Vue", + "description": "An enterprise-class UI design language and Vue-based implementation", + "keywords": [ + "ant", + "design", + "antd", + "vue", + "vueComponent", + "component", + "components", + "ui", + "framework", + "frontend" + ], + "main": "dist/antd.min.js", + "files": [ + "dist", + "lib", + "es" + ], + "scripts": { + "start": "NODE_ENV=development ./node_modules/.bin/webpack-dev-server --open --hot", + "test": "karma start test/karma.conf.js --single-run", + "site": "node scripts/run.js site-dist", + "copy": "node scripts/run.js copy-html", + "compile": "node antd-tools/cli/run.js compile", + "pub": "node antd-tools/cli/run.js pub", + "prepublish": "node antd-tools/cli/run.js guard", + "dist": "node antd-tools/cli/run.js dist", + "lint": "eslint -c ./.eslintrc --fix --ext .jsx,.js,.vue ./components", + "lint:style": "stylelint \"./examples/**/*.less\" --fix --syntax less", + "commitmsg": "validate-commit-msg", + "cm": "git-cz" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vueComponent/ant-design.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/vueComponent/ant-design/issues" + }, + "homepage": "https://github.com/vueComponent/ant-design", + "pre-commit": [ + "lint:style", + "lint" + ], + "devDependencies": { + "@octokit/rest": "^15.2.6", + "autoprefixer": "^8.1.0", + "babel-cli": "^6.26.0", + "babel-core": "^6.26.0", + "babel-eslint": "^8.0.1", + "babel-helper-vue-jsx-merge-props": "^2.0.3", + "babel-loader": "^7.1.2", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-import": "^1.1.1", + "babel-plugin-istanbul": "^4.1.1", + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "babel-plugin-transform-decorators": "^6.24.1", + "babel-plugin-transform-decorators-legacy": "^1.3.4", + "babel-plugin-transform-es3-member-expression-literals": "^6.22.0", + "babel-plugin-transform-es3-property-literals": "^6.22.0", + "babel-plugin-transform-object-assign": "^6.22.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-plugin-transform-runtime": "~6.23.0", + "babel-plugin-transform-vue-jsx": "^3.7.0", + "babel-polyfill": "^6.26.0", + "babel-preset-env": "^1.6.1", + "case-sensitive-paths-webpack-plugin": "^2.1.2", + "chai": "^4.1.2", + "chalk": "^2.3.2", + "cheerio": "^1.0.0-rc.2", + "colorful": "^2.1.0", + "commander": "^2.15.0", + "commitizen": "^2.9.6", + "css-loader": "^0.28.7", + "deep-assign": "^2.0.0", + "eslint": "^4.7.2", + "eslint-plugin-vue": "^3.13.0", + "eslint-plugin-html": "^3.2.2", + "eslint-plugin-vue-libs": "^1.2.1", + "extract-text-webpack-plugin": "^3.0.2", + "fetch-jsonp": "^1.1.3", + "gulp": "^3.9.1", + "gulp-babel": "^7.0.0", + "gulp-strip-code": "^0.1.4", + "highlight.js": "^9.12.0", + "html-webpack-plugin": "^2.30.1", + "husky": "^0.14.3", + "istanbul-instrumenter-loader": "^3.0.0", + "jsonp": "^0.2.1", + "karma": "^1.4.1", + "karma-coverage": "^1.1.1", + "karma-coverage-istanbul-reporter": "^1.3.0", + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-phantomjs-shim": "^1.4.0", + "karma-sinon-chai": "^1.3.1", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "0.0.31", + "karma-webpack": "^2.0.2", + "less": "^2.7.2", + "less-loader": "^4.0.5", + "less-plugin-npm-import": "^2.1.0", + "markdown-it": "^8.4.0", + "markdown-it-anchor": "^4.0.0", + "marked": "^0.3.7", + "merge2": "^1.2.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "mocha": "^3.2.0", + "postcss": "^6.0.20", + "postcss-loader": "^2.1.2", + "pre-commit": "^1.2.2", + "querystring": "^0.2.0", + "reqwest": "^2.0.5", + "rimraf": "^2.6.2", + "rucksack-css": "^1.0.2", + "selenium-server": "^3.0.1", + "semver": "^5.3.0", + "sinon": "^4.0.2", + "sinon-chai": "^2.8.0", + "style-loader": "^0.18.2", + "stylelint": "^8.1.1", + "stylelint-config-standard": "^17.0.0", + "through2": "^2.0.3", + "validate-commit-msg": "^2.14.0", + "vue": "^2.5.15", + "vue-antd-md-loader": "^1.0.3", + "vue-clipboard2": "0.0.8", + "vue-loader": "^13.0.5", + "vue-router": "^3.0.1", + "vue-template-compiler": "^2.5.15", + "webpack": "^3.6.0", + "webpack-chunk-hash": "^0.5.0", + "webpack-dev-server": "^2.8.2", + "webpack-merge": "^4.1.1" + }, + "dependencies": { + "add-dom-event-listener": "^1.0.2", + "array-tree-filter": "^2.1.0", + "async-validator": "^1.8.2", + "babel-helper-vue-jsx-merge-props": "^2.0.3", + "babel-runtime": "6.x", + "classnames": "^2.2.5", + "component-classes": "^1.2.6", + "css-animation": "^1.4.1", + "dom-align": "^1.6.7", + "dom-closest": "^0.2.0", + "dom-scroll-into-view": "^1.2.1", + "enquire.js": "^2.1.6", + "is-negative-zero": "^2.0.0", + "lodash": "^4.17.5", + "moment": "^2.21.0", + "omit.js": "^1.0.0", + "shallow-equal": "^1.0.0", + "shallowequal": "^1.0.2", + "warning": "^3.0.0" + } } diff --git a/site/routes.js b/site/routes.js index 6aa0a45f2..adae1220a 100644 --- a/site/routes.js +++ b/site/routes.js @@ -4,7 +4,7 @@ import Iframe from './components/iframe.vue' const AsyncTestComp = () => { const d = window.location.hash.replace('#', '') return { - component: import(`../components/menu/demo/${d}`), + component: import(`../components/vc-form/demo/${d}`), } } From c0bf96fccde8efd30fc2e4ad9681ccbc65681a8a Mon Sep 17 00:00:00 2001 From: tjz <415800467@qq.com> Date: Thu, 3 May 2018 22:53:17 +0800 Subject: [PATCH 13/39] feat: vc-form --- components/vc-form/src/createBaseForm.jsx | 60 +++++++++++++---------- components/vc-form/src/index.jsx | 10 ++++ package-lock.json | 28 ++++++++++- package.json | 3 +- 4 files changed, 72 insertions(+), 29 deletions(-) diff --git a/components/vc-form/src/createBaseForm.jsx b/components/vc-form/src/createBaseForm.jsx index 23c60d38e..4384cb712 100644 --- a/components/vc-form/src/createBaseForm.jsx +++ b/components/vc-form/src/createBaseForm.jsx @@ -18,7 +18,7 @@ import { flattenArray, } from './utils' -const DEFAULT_TRIGGER = 'onChange' +const DEFAULT_TRIGGER = 'change' function createBaseForm (option = {}, mixins = []) { const { @@ -94,7 +94,7 @@ function createBaseForm (option = {}, mixins = []) { const valuesAllSet = {} valuesAll[name] = value Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key])) - onValuesChange(this.props, set({}, name, value), valuesAllSet) + onValuesChange(this.$props, set({}, name, value), valuesAllSet) } const field = this.fieldsStore.getField(name) return ({ name, field: { ...field, value, touched: true }, fieldMeta }) @@ -148,7 +148,7 @@ function createBaseForm (option = {}, mixins = []) { }, getFieldDecorator (name, fieldOption) { - const { ref, ...restProps } = this.getFieldProps(name, fieldOption) + const { directives, props } = this.getFieldProps(name, fieldOption) return (fieldElem) => { const fieldMeta = this.fieldsStore.getFieldMeta(name) const originalProps = fieldElem.props @@ -173,10 +173,10 @@ function createBaseForm (option = {}, mixins = []) { fieldMeta.ref = fieldElem.ref return cloneElement(fieldElem, { props: { - ...restProps, + ...props, ...this.fieldsStore.getFieldValuePropValue(fieldMeta), }, - ref, + directives, }) } }, @@ -220,25 +220,10 @@ function createBaseForm (option = {}, mixins = []) { const inputProps = { ...this.fieldsStore.getFieldValuePropValue(fieldOption), - ref: name, + // ref: name, + on: {}, } const saveRef = this.getCacheBind(name, `${name}__ref`, this.saveRef) - this.$nextTick(() => { - if (this.instances[name] !== this.$refs[name]) { - this.$refs[name].destroyed = () => { - this.$refs[name].destroyed() - // 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] - } - } - saveRef(name, `${name}__ref`, this.$refs[name]) - }) if (fieldNameProp) { inputProps[fieldNameProp] = name } @@ -252,7 +237,7 @@ function createBaseForm (option = {}, mixins = []) { // make sure that the value will be collect if (trigger && validateTriggers.indexOf(trigger) === -1) { - inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect) + inputProps.on[trigger] = this.getCacheBind(name, trigger, this.onCollect) } const meta = { @@ -269,7 +254,14 @@ function createBaseForm (option = {}, mixins = []) { inputProps[fieldDataProp] = this.fieldsStore.getField(name) } - return inputProps + return { + props: inputProps, + directives: [ + { name: 'ant-form-item-ref-cal', value: (component) => { + saveRef(component) + } }, + ], + } }, getFieldInstance (name) { @@ -289,9 +281,12 @@ function createBaseForm (option = {}, mixins = []) { if (onFieldsChange) { const changedFields = Object.keys(fields) .reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {}) - onFieldsChange(this.props, changedFields, this.fieldsStore.getNestedAllFields()) + onFieldsChange(this.$props, changedFields, this.fieldsStore.getNestedAllFields()) } - this.forceUpdate(callback) + this.$forceUpdate() + this.$nextTick(() => { + callback && callback() + }) }, resetFields (ns) { @@ -330,11 +325,22 @@ function createBaseForm (option = {}, mixins = []) { this.setFields(newFields, callback) if (onValuesChange) { const allValues = this.fieldsStore.getAllValues() - onValuesChange(this.props, changedValues, allValues) + 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) { diff --git a/components/vc-form/src/index.jsx b/components/vc-form/src/index.jsx index ad1f4f338..d54a498b7 100644 --- a/components/vc-form/src/index.jsx +++ b/components/vc-form/src/index.jsx @@ -2,5 +2,15 @@ import createForm from './createForm' import createFormField from './createFormField' import formShape from './propTypes' +import Vue from 'vue' + +Vue.directive('ant-form-item-ref-cal', { + inserted: function (el, binding, vnode) { + binding.value(vnode) + }, + unbind: function (el, binding, vnode) { + binding.value() + }, +}) export { createForm, createFormField, formShape } diff --git a/package-lock.json b/package-lock.json index d4b851fe9..884880a88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vue-antd-ui", - "version": "0.4.0", + "version": "0.4.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -620,6 +620,14 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "async-validator": { + "version": "1.8.2", + "resolved": "http://registry.npm.taobao.org/async-validator/download/async-validator-1.8.2.tgz", + "integrity": "sha1-t3WXIm6WJC+NUxwNRq4pX2JCK6Q=", + "requires": { + "babel-runtime": "6.26.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1044,6 +1052,12 @@ "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", "dev": true }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "http://registry.npm.taobao.org/babel-plugin-syntax-class-properties/download/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, "babel-plugin-syntax-decorators": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", @@ -1091,6 +1105,18 @@ "babel-runtime": "6.26.0" } }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "http://registry.npm.taobao.org/babel-plugin-transform-class-properties/download/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-plugin-syntax-class-properties": "6.13.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, "babel-plugin-transform-decorators": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", diff --git a/package.json b/package.json index 404d4bed9..cb1430ec5 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "babel-plugin-istanbul": "^4.1.1", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-syntax-jsx": "^6.18.0", + "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-decorators": "^6.24.1", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-es3-member-expression-literals": "^6.22.0", @@ -81,8 +82,8 @@ "css-loader": "^0.28.7", "deep-assign": "^2.0.0", "eslint": "^4.7.2", - "eslint-plugin-vue": "^3.13.0", "eslint-plugin-html": "^3.2.2", + "eslint-plugin-vue": "^3.13.0", "eslint-plugin-vue-libs": "^1.2.1", "extract-text-webpack-plugin": "^3.0.2", "fetch-jsonp": "^1.1.3", From 4f0c5b8ce123e2d04a79e85a71fd6c8645aee138 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Fri, 4 May 2018 12:16:17 +0800 Subject: [PATCH 14/39] feat: add vc-form demo --- components/_util/vnode.js | 3 +- components/vc-form/demo/async-init.js | 123 ++++++++++++ components/vc-form/demo/dynamic-fields.js | 201 +++++++++++++++++++ components/vc-form/demo/dynamic.js | 131 ++++++++++++ components/vc-form/demo/file-input.js | 83 ++++++++ components/vc-form/demo/getFieldDecorator.js | 55 +++++ components/vc-form/src/createBaseForm.jsx | 77 ++++--- components/vc-form/src/index.jsx | 4 +- 8 files changed, 645 insertions(+), 32 deletions(-) create mode 100644 components/vc-form/demo/async-init.js create mode 100644 components/vc-form/demo/dynamic-fields.js create mode 100644 components/vc-form/demo/dynamic.js create mode 100644 components/vc-form/demo/file-input.js create mode 100644 components/vc-form/demo/getFieldDecorator.js diff --git a/components/_util/vnode.js b/components/_util/vnode.js index 4f7e91e0c..a7e40ec98 100644 --- a/components/_util/vnode.js +++ b/components/_util/vnode.js @@ -60,7 +60,7 @@ export function cloneElement (n, nodeProps, deep) { return null } const node = cloneVNode(ele, deep) - const { props = {}, key, on = {}, children } = nodeProps + const { props = {}, key, on = {}, children, directives = [] } = nodeProps const data = node.data || {} let cls = {} let style = {} @@ -101,6 +101,7 @@ export function cloneElement (n, nodeProps, deep) { class: cls, domProps: { ...data.domProps, ...domProps }, scopedSlots: { ...data.scopedSlots, ...scopedSlots }, + directives: [...(data.directives || []), ...directives], }) if (node.componentOptions) { diff --git a/components/vc-form/demo/async-init.js b/components/vc-form/demo/async-init.js new file mode 100644 index 000000000..e46039e1f --- /dev/null +++ b/components/vc-form/demo/async-init.js @@ -0,0 +1,123 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import { regionStyle, errorStyle } from './styles' +import BaseMixin from '../../_util/BaseMixin' + +const Email = { + props: { + form: Object, + }, + methods: { + checkSpecial (rule, value, callback) { + setTimeout(() => { + if (value === 'yiminghe@gmail.com') { + callback('can not be!') + } else { + callback() + } + }, 1000) + }, + }, + + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('email') + return (
+
email validate onBlur
+
+
+
+ {errors ? errors.join(',') : null} +
+
+ {isFieldValidating('email') ? 'validating' : null} +
+
) + }, +} + +const Form = { + mixins: [BaseMixin], + props: { + form: Object, + }, + data () { + return { + loading: true, + } + }, + + mounted () { + setTimeout(() => { + this.setState({ + loading: false, + }, () => { + setTimeout(() => { + this.form.setFieldsInitialValue({ + email: 'xx@gmail.com', + }) + }, 1000) + }) + }, 1000) + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.submit((callback) => { + setTimeout(() => { + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + callback() + }) + }, 1000) + }) + }, + + reset (e) { + e.preventDefault() + this.form.resetFields() + }, + }, + + render () { + if (this.loading) { + return loading + } + const { form } = this + const disabled = form.isFieldsValidating() || form.isSubmitting() + return (
+

async init field

+
+ + +
+ +  {disabled ? disabled : null}  + +
+ +
) + }, +} + +export default createForm()(Form) diff --git a/components/vc-form/demo/dynamic-fields.js b/components/vc-form/demo/dynamic-fields.js new file mode 100644 index 000000000..5bf0fa7e7 --- /dev/null +++ b/components/vc-form/demo/dynamic-fields.js @@ -0,0 +1,201 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import BaseMixin from '../../_util/BaseMixin' + +const Form1 = { + mixins: [BaseMixin], + props: { + form: Object, + }, + data () { + return { + useInput: true, + } + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + changeUseInput (e) { + this.setState({ + useInput: e.target.checked, + }) + }, + }, + + render () { + const { getFieldError, getFieldDecorator } = this.form + + return ( +
+

situation 1

+ {this.useInput ? getFieldDecorator('name', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s your name 1?', + }], + })() : null} + text content + {this.useInput ? null : getFieldDecorator('name', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s your name 2?', + }], + })()} +
+ + {(getFieldError('name') || []).join(', ')} +
+ +
+ ) + }, +} + +const Form2 = { + mixins: [BaseMixin], + props: { + form: Object, + }, + data () { + return { + useInput: true, + } + }, + beforeMount () { + const { getFieldDecorator } = this.form + this.nameDecorator = getFieldDecorator('name', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s your name?', + }], + }) + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + changeUseInput (e) { + this.setState({ + useInput: e.target.checked, + }) + }, + }, + + render () { + const { getFieldError } = this.form + return ( +
+

situation 2

+ {this.useInput ? this.nameDecorator() : null} + text content + {this.useInput ? null : this.nameDecorator()} +
+ + {(getFieldError('name') || []).join(', ')} +
+ +
+ ) + }, +} + +const Form3 = { + mixins: [BaseMixin], + props: { + form: Object, + }, + data () { + return { + useInput: false, + } + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + changeUseInput (e) { + this.setState({ + useInput: e.target.checked, + }) + }, + }, + + render () { + const { getFieldError, getFieldDecorator } = this.form + return ( +
+

situation 3

+ {getFieldDecorator('name', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s your name 1?', + }], + })()} + {this.useInput ? null : getFieldDecorator('name2', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s your name 2?', + }], + })()} +
+ + {(getFieldError('name') || []).join(', ')} +
+ +
+ ) + }, +} + +const WrappedForm1 = createForm()(Form1) +const WrappedForm2 = createForm()(Form2) +const WrappedForm3 = createForm()(Form3) + +export default { + render () { + return ( +
+ + + +
+ ) + }, +} diff --git a/components/vc-form/demo/dynamic.js b/components/vc-form/demo/dynamic.js new file mode 100644 index 000000000..dd56eaaa7 --- /dev/null +++ b/components/vc-form/demo/dynamic.js @@ -0,0 +1,131 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import { regionStyle, errorStyle } from './styles' + +const Email = { + props: { + form: Object, + hidden: Boolean, + }, + render () { + const { hidden, form } = this + const { getFieldProps, getFieldError, isFieldValidating } = form + const errors = getFieldError('email') + const style = { + ...regionStyle, + display: hidden ? 'none' : '', + } + return (
+
email: +
+ + {errors ?
{errors.join(',')}
: null} + + {isFieldValidating('email') ?
validating
: null} +
) + }, + +} + +const User = { + props: { + form: Object, + }, + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('user') + return (
+
user: + +
+ {errors ?
{errors.join(',')}
: null} + + {isFieldValidating('user') ?
validating
: null} +
) + }, +} + +const Form = { + props: { + form: Object, + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + }, + + render () { + const { form } = this + const { getFieldProps, getFieldValue } = form + return (
+

overview

+
+
+
+ +
+
+ + { getFieldValue('remove_user') ? null : } + +
+
+ +
+
+ +
) + }, +} + +export default createForm()(Form) diff --git a/components/vc-form/demo/file-input.js b/components/vc-form/demo/file-input.js new file mode 100644 index 000000000..0a723a52e --- /dev/null +++ b/components/vc-form/demo/file-input.js @@ -0,0 +1,83 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import { regionStyle, errorStyle } from './styles' + +function getFileValueProps (value) { + if (value && value.target) { + return { + value: value.target.value, + } + } + return { + value, + } +} + +function getValueFromFileEvent ({ target }) { + return { + target, + } +} + +const Form = { + props: { + form: Object, + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + console.log(error, values) + if (!error) { + console.log('校验通过') + } + }) + }, + checkSize (rule, value, callback) { + if (value && value.target) { + const files = value.target.files + if (files[0]) { + callback(files[0].size > 1000000 ? 'file size must be less than 1M' : undefined) + } else { + callback() + } + } else { + callback() + } + }, + }, + + render () { + const { getFieldProps, getFieldError } = this.form + const errors = getFieldError('attachment') + return (
+
attachment:
+
+ +
+
+ {(errors) ? errors.join(',') : null} +
+ +
) + }, +} + +const NewForm = createForm()(Form) + +export default { + render () { + return (
+

input[type="file"]

+ +
) + }, +} diff --git a/components/vc-form/demo/getFieldDecorator.js b/components/vc-form/demo/getFieldDecorator.js new file mode 100644 index 000000000..76285870f --- /dev/null +++ b/components/vc-form/demo/getFieldDecorator.js @@ -0,0 +1,55 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' + +const Form = { + props: { + form: Object, + }, + + beforeMount () { + this.nameDecorator = this.form.getFieldDecorator('name', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s your name?', + }], + }) + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + + onChange (e) { + console.log(e.target.value) + }, + }, + + render () { + const { getFieldError } = this.form + + return ( +
+ {this.nameDecorator( + + )} +
+ {(getFieldError('name') || []).join(', ')} +
+ +
+ ) + }, +} + +export default createForm()(Form) diff --git a/components/vc-form/src/createBaseForm.jsx b/components/vc-form/src/createBaseForm.jsx index 4384cb712..07a85baf7 100644 --- a/components/vc-form/src/createBaseForm.jsx +++ b/components/vc-form/src/createBaseForm.jsx @@ -5,7 +5,9 @@ import set from 'lodash/set' import createFieldsStore from './createFieldsStore' import { cloneElement } from '../../_util/vnode' import BaseMixin from '../../_util/BaseMixin' -import PropTypes from '../../_util/vue-types' +import { getOptionProps, getEvents } from '../../_util/props-util' +// import PropTypes from '../../_util/vue-types' + import { argumentContainer, identity, @@ -18,7 +20,7 @@ import { flattenArray, } from './utils' -const DEFAULT_TRIGGER = 'change' +const DEFAULT_TRIGGER = 'input' function createBaseForm (option = {}, mixins = []) { const { @@ -38,10 +40,10 @@ function createBaseForm (option = {}, mixins = []) { return function decorate (WrappedComponent) { const Form = { mixins: [BaseMixin, ...mixins], - props: { - hideRequiredMark: PropTypes.bool, - layout: PropTypes.string, - }, + // props: { + // hideRequiredMark: PropTypes.bool, + // layout: PropTypes.string, + // }, data () { const fields = mapPropsToFields && mapPropsToFields(this.$props) this.fieldsStore = createFieldsStore(fields || {}) @@ -148,10 +150,11 @@ function createBaseForm (option = {}, mixins = []) { }, getFieldDecorator (name, fieldOption) { - const { directives, props } = this.getFieldProps(name, fieldOption) + const { props, ...restProps } = this.getFieldProps(name, fieldOption) return (fieldElem) => { const fieldMeta = this.fieldsStore.getFieldMeta(name) - const originalProps = fieldElem.props + const originalProps = getOptionProps(fieldElem) + const originalEvents = getEvents(fieldElem) if (process.env.NODE_ENV !== 'production') { const valuePropName = fieldMeta.valuePropName warning( @@ -170,14 +173,26 @@ function createBaseForm (option = {}, mixins = []) { ) } fieldMeta.originalProps = originalProps - fieldMeta.ref = fieldElem.ref - return cloneElement(fieldElem, { + // fieldMeta.ref = fieldElem.data && fieldElem.data.ref + const newProps = { props: { ...props, ...this.fieldsStore.getFieldValuePropValue(fieldMeta), }, - directives, + ...restProps, + } + newProps.domProps.value = newProps.props.value + const newEvents = {} + Object.keys(newProps.on).forEach((key) => { + if (originalEvents[key]) { + const triggerEvents = newProps.on[key] + newEvents[key] = (...args) => { + originalEvents[key](...args) + triggerEvents(...args) + } + } }) + return cloneElement(fieldElem, { ...newProps, on: newEvents }) } }, @@ -221,9 +236,8 @@ function createBaseForm (option = {}, mixins = []) { const inputProps = { ...this.fieldsStore.getFieldValuePropValue(fieldOption), // ref: name, - on: {}, } - const saveRef = this.getCacheBind(name, `${name}__ref`, this.saveRef) + const inputListeners = {} if (fieldNameProp) { inputProps[fieldNameProp] = name } @@ -231,13 +245,13 @@ function createBaseForm (option = {}, mixins = []) { 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) + if (inputListeners[action]) return + inputListeners[action] = this.getCacheBind(name, action, this.onCollectValidate) }) // make sure that the value will be collect if (trigger && validateTriggers.indexOf(trigger) === -1) { - inputProps.on[trigger] = this.getCacheBind(name, trigger, this.onCollect) + inputListeners[trigger] = this.getCacheBind(name, trigger, this.onCollect) } const meta = { @@ -256,11 +270,16 @@ function createBaseForm (option = {}, mixins = []) { return { props: inputProps, + domProps: { + value: inputProps.value, + }, directives: [ - { name: 'ant-form-item-ref-cal', value: (component) => { - saveRef(component) - } }, + { + name: 'ant-ref', + value: this.getCacheBind(name, `${name}__ref`, this.saveRef), + }, ], + on: inputListeners, } }, @@ -342,16 +361,16 @@ function createBaseForm (option = {}, mixins = []) { 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) - } - } + // 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 }, diff --git a/components/vc-form/src/index.jsx b/components/vc-form/src/index.jsx index d54a498b7..4d3bdf6d8 100644 --- a/components/vc-form/src/index.jsx +++ b/components/vc-form/src/index.jsx @@ -4,8 +4,8 @@ import createFormField from './createFormField' import formShape from './propTypes' import Vue from 'vue' -Vue.directive('ant-form-item-ref-cal', { - inserted: function (el, binding, vnode) { +Vue.directive('ant-ref', { + bind: function (el, binding, vnode) { binding.value(vnode) }, unbind: function (el, binding, vnode) { From 0658674d3f3d55749e8f82c39f0ef86a6f5a2f5a Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Fri, 4 May 2018 16:02:31 +0800 Subject: [PATCH 15/39] feat: add vc-form demo --- components/_util/ContainerRender.jsx | 2 + components/_util/antRefDirective.js | 12 + components/modal/confirm.js | 4 + components/trigger/Trigger.jsx | 2 + components/vc-form/demo/modal.js | 84 +++++++ components/vc-form/demo/nested-field.js | 194 +++++++++++++++ components/vc-form/demo/normalize.js | 151 ++++++++++++ components/vc-form/demo/overview.js | 259 ++++++++++++++++++++ components/vc-form/src/createBaseForm.jsx | 15 +- components/vc-form/src/createDOMForm.jsx | 4 +- components/vc-form/src/index.jsx | 11 +- components/vc-form/src/utils.js | 2 +- components/vc-notification/Notification.jsx | 3 + 13 files changed, 726 insertions(+), 17 deletions(-) create mode 100644 components/_util/antRefDirective.js create mode 100644 components/vc-form/demo/modal.js create mode 100644 components/vc-form/demo/nested-field.js create mode 100644 components/vc-form/demo/normalize.js create mode 100644 components/vc-form/demo/overview.js diff --git a/components/_util/ContainerRender.jsx b/components/_util/ContainerRender.jsx index 9540e6db4..523922b95 100644 --- a/components/_util/ContainerRender.jsx +++ b/components/_util/ContainerRender.jsx @@ -1,6 +1,8 @@ import Vue from 'vue' import PropTypes from './vue-types' +import antRefDirective from './antRefDirective' +Vue.use(antRefDirective) export default { props: { diff --git a/components/_util/antRefDirective.js b/components/_util/antRefDirective.js new file mode 100644 index 000000000..c7d0afde4 --- /dev/null +++ b/components/_util/antRefDirective.js @@ -0,0 +1,12 @@ +export default { + install: (Vue, options) => { + Vue.directive('ant-ref', { + bind: function (el, binding, vnode) { + binding.value(vnode) + }, + unbind: function (el, binding, vnode) { + binding.value() + }, + }) + }, +} diff --git a/components/modal/confirm.js b/components/modal/confirm.js index 69ac8082d..fc2f7913a 100644 --- a/components/modal/confirm.js +++ b/components/modal/confirm.js @@ -1,5 +1,9 @@ import Vue from 'vue' import ConfirmDialog from './ConfirmDialog' + +import antRefDirective from '../_util/antRefDirective' +Vue.use(antRefDirective) + export default function confirm (config) { const div = document.createElement('div') const el = document.createElement('div') diff --git a/components/trigger/Trigger.jsx b/components/trigger/Trigger.jsx index 547411e7f..731b27425 100644 --- a/components/trigger/Trigger.jsx +++ b/components/trigger/Trigger.jsx @@ -10,6 +10,8 @@ import Popup from './Popup' import { getAlignFromPlacement, getPopupClassNameFromAlign, noop } from './utils' import BaseMixin from '../_util/BaseMixin' import { cloneElement } from '../_util/vnode' +import antRefDirective from '../_util/antRefDirective' +Vue.use(antRefDirective) function returnEmptyString () { return '' diff --git a/components/vc-form/demo/modal.js b/components/vc-form/demo/modal.js new file mode 100644 index 000000000..a9d127886 --- /dev/null +++ b/components/vc-form/demo/modal.js @@ -0,0 +1,84 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import BaseMixin from '../../_util/BaseMixin' +import createDOMForm from '../src/createDOMForm' +import { Modal } from 'antd' +import { regionStyle, errorStyle } from './styles' + +const Form = { + mixins: [BaseMixin], + props: { + form: Object, + }, + + data () { + return { + visible: false, + } + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFieldsAndScroll((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + + onCancel () { + this.setState({ + visible: false, + }) + }, + + open () { + this.setState({ + visible: true, + }) + }, + }, + + render () { + const { getFieldProps, getFieldError } = this.form + return (
+

modal

+ +
+
+ +
+ {getFieldError('required') ? getFieldError('required').join(',') + : 1} +
+
+ +
+
+
+
+
+ +
+
) + }, +} + +export default createDOMForm()(Form) diff --git a/components/vc-form/demo/nested-field.js b/components/vc-form/demo/nested-field.js new file mode 100644 index 000000000..a04bdb271 --- /dev/null +++ b/components/vc-form/demo/nested-field.js @@ -0,0 +1,194 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import createForm from '../src/createDOMForm' + +const Form = { + props: { + form: Object, + }, + methods: { + onSubmit (e) { + e.preventDefault() + console.log('Values of member[0].name.firstname and a[0][1].b.c[0]') + console.log(this.form.getFieldsValue(['member[0].name.firstname', 'a[0][1].b.c[0]'])) + console.log('Values of all fields') + console.log(this.form.getFieldsValue()) + + this.form.validateFieldsAndScroll((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + + onChange (e) { + console.log(e.target.value) + }, + + setField () { + this.form.setFieldsValue({ + member: [ + { + name: { + firstname: 'm1 first', + lastname: 'm1 last', + }, + }, + { + name: { + firstname: 'm2 first', + lastname: 'm2 last', + }, + }, + ], + a: [ + [undefined, { + b: { + c: ['Value of a[0][1].b.c[0]'], + }, + }], + ], + w: { + x: { + y: { + z: ['Value of w.x.y.z[0]'], + }, + }, + }, + }) + }, + + resetFields () { + console.log('reset') + this.form.resetFields() + }, + }, + + render () { + const { getFieldDecorator, getFieldError } = this.form + + return ( +
+
Member 0 firstname
+ {getFieldDecorator('member[0].name.firstname', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s the member_0 firstname?', + }], + })( + + )} +
+ {(getFieldError('member[0].name.firstname') || []).join(', ')} +
+ +
Member 0 lastname
+ {getFieldDecorator('member[0].name.lastname', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s the member_0 lastname?', + }], + })( + + )} +
+ {(getFieldError('member[0].name.firstname') || []).join(', ')} +
+ +
Member 1 firstname
+ {getFieldDecorator('member[1].name.firstname', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s the member_1 fistname?', + }], + })( + + )} +
+ {(getFieldError('member[1].name.firstname') || []).join(', ')} +
+ +
Member 1 lastname
+ {getFieldDecorator('member[1].name.lastname', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s the member_1 lastname?', + }], + })( + + )} +
+ {(getFieldError('member[1].name.firstname') || []).join(', ')} +
+ +
a[0][1].b.c[0]
+ {getFieldDecorator('a[0][1].b.c[0]', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s a[0][1].b.c[0]?', + }], + })( + + )} +
+ {(getFieldError('a[0][1].b.c[0]') || []).join(', ')} +
+ +
w.x.y.z[0]
+ {getFieldDecorator('w.x.y.z[0]', { + initialValue: '', + rules: [{ + required: true, + message: 'What\'s w.x.y.z[0]?', + }], + })( + + )} +
+ {(getFieldError('w.x.y.z[0]') || []).join(', ')} +
+ + + + +
+ ) + }, +} + +const NewForm = createForm({ + onFieldsChange (_, changedFields, allFields) { + console.log('onFieldsChange: ', changedFields, allFields) + }, + onValuesChange (_, changedValues, allValues) { + console.log('onValuesChange: ', changedValues, allValues) + }, +})(Form) + +export default { + render () { + return (
+

setFieldsValue

+ +
) + }, +} diff --git a/components/vc-form/demo/normalize.js b/components/vc-form/demo/normalize.js new file mode 100644 index 000000000..4de51e37f --- /dev/null +++ b/components/vc-form/demo/normalize.js @@ -0,0 +1,151 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import { regionStyle, errorStyle } from './styles' + +const CustomInput = { + props: { + form: Object, + }, + data () { + return { + data: [], + } + }, + methods: { + checkUpper (rule, value = '', callback) { + if (value !== value.toUpperCase()) { + callback(new Error('need to be upper!')) + } else { + callback() + } + }, + toUpper (v, prev) { + if (v === prev) { + return v + } + return v.toUpperCase() + }, + }, + + render () { + const { getFieldProps, getFieldError } = this.form + const errors = getFieldError('upper') + return (
+
upper normalize
+
+ +
+
+ {(errors) ? errors.join(',') : null} +
+
) + }, +} + +const MaxMin = { + props: { + form: Object, + }, + methods: { + normalizeMin (value, prevValue, allValues) { + console.log('normalizeMin', allValues.min, allValues.max) + const previousAllValues = this.form.getFieldsValue() + if (allValues.max !== previousAllValues.max) { + // max changed + if (value === '' || Number(allValues.max) < Number(value)) { + return allValues.max + } + } + return value + }, + normalizeMax (value, prevValue, allValues) { + console.log('normalizeMax', allValues.min, allValues.max) + const previousAllValues = this.form.getFieldsValue() + if (allValues.min !== previousAllValues.min) { + // min changed + if (value === '' || Number(allValues.min) > Number(value)) { + return allValues.min + } + } + return value + }, + }, + + render () { + const { getFieldProps } = this.form + return (
+
min: +
+
max: +
+
) + }, +} + +const Form = { + // props: { + // form: Object, + // }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + }, + + render () { + const { form } = this + return (
+

normalize

+
+ + + + +
+ +
+ +
) + }, +} + +export default createForm()(Form) + diff --git a/components/vc-form/demo/overview.js b/components/vc-form/demo/overview.js new file mode 100644 index 000000000..b4785c2a8 --- /dev/null +++ b/components/vc-form/demo/overview.js @@ -0,0 +1,259 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import createDOMForm from '../src/createDOMForm' +import { DatePicker, Select } from 'antd' +import { regionStyle, errorStyle } from './styles' +const { Option } = Select + +const Email = { + props: { + form: Object, + }, + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('email') + return (
+
email sync validate
+
+ 错误的 email 格式, + }, + ], + })} + />
+
+ {errors} +
+
+ {isFieldValidating('email') ? 'validating' : null} +
+
) + }, + +} + +const User = { + props: { + form: Object, + }, + methods: { + userExists (rule, value, callback) { + setTimeout(() => { + if (value === '1') { + callback([new Error('are you kidding?')]) + } else if (value === 'yiminghe') { + callback([new Error('forbid yiminghe')]) + } else { + callback() + } + }, 300) + }, + }, + + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('user') + return (
+
* user async validate
+
+
+
+ {(errors) ? errors.join(',') : null} +
+
+ {isFieldValidating('user') ? 'validating' : null} +
+
) + }, +} + +const CustomInput = { + props: { + form: Object, + }, + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('select') + return (
+
* custom select sync validate
+
+
+ {(errors) ? errors.join(',') : null} +
+
+ {isFieldValidating('select') ? 'validating' : null} +
+
) + }, + +} + +const DateInput = { + props: { + form: Object, + }, + render () { + const { getFieldProps, getFieldError } = this.form + const errors = getFieldError('date') + return (
+
* DateInput sync validate
+
+ +
+
+ {(errors) ? errors.join(',') : null} +
+
) + }, +} + +function toNumber (v) { + if (v === undefined) { + return v + } + if (v === '') { + return undefined + } + if (v && v.trim() === '') { + return NaN + } + return Number(v) +} + +const NumberInput = { + props: { + form: Object, + }, + render () { + const { getFieldProps, getFieldError } = this.form + const errors = getFieldError('number') + return (
+
number input
+
+ +
+
+ {(errors) ? errors.join(',') : null} +
+
) + }, + +} + +const Form = { + methods: { + onSubmit (e) { + console.log('submit') + e.preventDefault() + this.form.validateFieldsAndScroll({ scroll: { offsetTop: 20 }}, (error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + + reset (e) { + e.preventDefault() + this.form.resetFields() + }, + }, + + render () { + const { form } = this + const { getFieldProps, getFieldError } = form + return (
+

overview

+
+ + + + + + + + + + +
+
normal required input
+
+ +
+
+ {(getFieldError('normal')) ? getFieldError('normal').join(',') : null} +
+
+ +
+ +   + +
+ +
) + }, +} + +export default createDOMForm({ + validateMessages: { + required (field) { + return `${field} 必填` + }, + }, +})(Form) diff --git a/components/vc-form/src/createBaseForm.jsx b/components/vc-form/src/createBaseForm.jsx index 07a85baf7..dd482bf80 100644 --- a/components/vc-form/src/createBaseForm.jsx +++ b/components/vc-form/src/createBaseForm.jsx @@ -20,7 +20,7 @@ import { flattenArray, } from './utils' -const DEFAULT_TRIGGER = 'input' +const DEFAULT_TRIGGER = 'change' function createBaseForm (option = {}, mixins = []) { const { @@ -534,15 +534,15 @@ function createBaseForm (option = {}, mixins = []) { }, render () { - const { $props, $listeners } = this + const { $listeners } = this const formProps = { [formPropName]: this.getForm(), } - + const props = getOptionProps(this) const wrappedComponentProps = { props: mapProps.call(this, { ...formProps, - ...$props, + ...props, }), on: $listeners, } @@ -552,7 +552,12 @@ function createBaseForm (option = {}, mixins = []) { return }, } - + if (!(WrappedComponent.props && formPropName in WrappedComponent.props)) { + WrappedComponent.props = { + ...WrappedComponent.props, + [formPropName]: Object, + } + } return argumentContainer(Form, WrappedComponent) } } diff --git a/components/vc-form/src/createDOMForm.jsx b/components/vc-form/src/createDOMForm.jsx index 9fdc5dc12..ed4393017 100644 --- a/components/vc-form/src/createDOMForm.jsx +++ b/components/vc-form/src/createDOMForm.jsx @@ -52,7 +52,7 @@ const mixin = { methods: { getForm () { return { - ...formMixin.getForm.call(this), + ...formMixin.methods.getForm.call(this), validateFieldsAndScroll: this.validateFieldsAndScroll, } }, @@ -69,7 +69,7 @@ const mixin = { if (has(error, name)) { const instance = this.getFieldInstance(name) if (instance) { - const node = instance.$el + const node = instance.$el || instance.elm const top = node.getBoundingClientRect().top if (firstTop === undefined || firstTop > top) { firstTop = top diff --git a/components/vc-form/src/index.jsx b/components/vc-form/src/index.jsx index 4d3bdf6d8..344a848c6 100644 --- a/components/vc-form/src/index.jsx +++ b/components/vc-form/src/index.jsx @@ -3,14 +3,7 @@ import createForm from './createForm' import createFormField from './createFormField' import formShape from './propTypes' import Vue from 'vue' - -Vue.directive('ant-ref', { - bind: function (el, binding, vnode) { - binding.value(vnode) - }, - unbind: function (el, binding, vnode) { - binding.value() - }, -}) +import antRefDirective from '../../_util/antRefDirective' +Vue.use(antRefDirective) export { createForm, createFormField, formShape } diff --git a/components/vc-form/src/utils.js b/components/vc-form/src/utils.js index 7819380bc..626cae283 100644 --- a/components/vc-form/src/utils.js +++ b/components/vc-form/src/utils.js @@ -6,7 +6,7 @@ export function argumentContainer (Container, WrappedComponent) { /* eslint no-param-reassign:0 */ Container.name = `Form_${getDisplayName(WrappedComponent)}` Container.WrappedComponent = WrappedComponent - Container.methods = { ...Container.methods, ...WrappedComponent.methods } + Container.props = { ...Container.props, ...WrappedComponent.props } return Container } diff --git a/components/vc-notification/Notification.jsx b/components/vc-notification/Notification.jsx index 81088375f..37199baff 100644 --- a/components/vc-notification/Notification.jsx +++ b/components/vc-notification/Notification.jsx @@ -7,6 +7,9 @@ import createChainedFunction from '../_util/createChainedFunction' import getTransitionProps from '../_util/getTransitionProps' import Notice from './Notice' +import antRefDirective from '../_util/antRefDirective' +Vue.use(antRefDirective) + let seed = 0 const now = Date.now() From 824666852df9f88956fd0d6d39c4282cbeec8ef4 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Fri, 4 May 2018 19:11:42 +0800 Subject: [PATCH 16/39] feat: add vc-form demo --- components/vc-form/demo/parallel-form.js | 85 ++++++++++++++ components/vc-form/demo/start-end-date.js | 91 +++++++++++++++ components/vc-form/demo/suggest.js | 120 ++++++++++++++++++++ components/vc-form/demo/validateFirst.js | 132 ++++++++++++++++++++++ components/vc-form/src/createBaseForm.jsx | 6 +- 5 files changed, 431 insertions(+), 3 deletions(-) create mode 100644 components/vc-form/demo/parallel-form.js create mode 100644 components/vc-form/demo/start-end-date.js create mode 100644 components/vc-form/demo/suggest.js create mode 100644 components/vc-form/demo/validateFirst.js diff --git a/components/vc-form/demo/parallel-form.js b/components/vc-form/demo/parallel-form.js new file mode 100644 index 000000000..d4403d835 --- /dev/null +++ b/components/vc-form/demo/parallel-form.js @@ -0,0 +1,85 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import { regionStyle } from './styles' +import { Switch } from 'antd' + +const TopForm = { + props: { + form: Object, + }, + render () { + const { getFieldProps } = this.form + return (
+
has email?
+
+ +
+
) + }, +} + +const BottomForm = { + props: { + form: Object, + on: Boolean, + }, + render () { + const { form } = this + const on = form.getFieldValue('on') + const style = { + ...regionStyle, + display: on ? 'block' : 'none', + } + return (
+
email:
+
+ +
+
) + }, +} + +const Form = { + props: { + form: Object, + }, + methods: { + onSubmit (e) { + e.preventDefault() + console.log(this.form.getFieldsValue()) + }, + }, + + render () { + const { form } = this + return (
+ + +
+ +
+
) + }, +} + +const NewForm = createForm()(Form) + +export default { + render () { + return (
+

parallel form

+ +
) + }, +} diff --git a/components/vc-form/demo/start-end-date.js b/components/vc-form/demo/start-end-date.js new file mode 100644 index 000000000..80993ad73 --- /dev/null +++ b/components/vc-form/demo/start-end-date.js @@ -0,0 +1,91 @@ +/* eslint no-console:0 */ + +import { DatePicker } from 'antd' +import createDOMForm from '../src/createDOMForm' +import { regionStyle, errorStyle } from './styles' + +const Form = { + props: { + form: Object, + }, + methods: { + onSubmit (e) { + console.log('submit') + e.preventDefault() + this.form.validateFieldsAndScroll((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + + reset (e) { + e.preventDefault() + this.form.resetFields() + }, + + checkStart (rule, value, callback) { + const { validateFields } = this.form + validateFields(['end'], { + force: true, + }) + callback() + }, + + checkEnd (rule, value, callback) { + const end = value + const { getFieldValue } = this.form + const start = getFieldValue('start') + if (!end || !start) { + callback('please select both start and end time') + } else if (end.valueOf() < start.valueOf()) { + callback('start time should be less than end time') + } else { + callback() + } + }, + }, + + render () { + const { form } = this + const { getFieldProps, getFieldError } = form + return (
+

startTime and endTime validation

+
+
+
start:
+
+ +
+
+ +
+
end:
+
+ +
+
+ +
+ {getFieldError('end') ? getFieldError('end').join(',') : ''} +
+ +
+ +   + +
+
+
) + }, +} + +export default createDOMForm()(Form) diff --git a/components/vc-form/demo/suggest.js b/components/vc-form/demo/suggest.js new file mode 100644 index 000000000..9d50e76fc --- /dev/null +++ b/components/vc-form/demo/suggest.js @@ -0,0 +1,120 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import { Select } from 'antd' +import { regionStyle, errorStyle } from './styles' +import { mergeProps } from '../../_util/props-util' +const emailTpl = ['@gmail.com', '@outlook.com', '@qq.com'] +const { Option } = Select +const CustomInput = { + props: { + form: Object, + }, + data () { + return { + data: [], + } + }, + methods: { + onChange (v) { + if (v.indexOf('@') === -1) { + this.data = emailTpl.map(m => v + m) + } else if (this.data.length) { + this.data = [] + } + }, + }, + + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('select') + return (
+
custom select sync validate
+
+ +
+
+ {(errors) ? errors.join(',') + : + 1 + } +
+
+ {isFieldValidating('select') ? 'validating' : + 1 + } +
+
) + }, +} + +const Form = { + props: { + form: Object, + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + }, + + render () { + const { form } = this + return (
+

suggest

+
+ + +
+ +
+ +
) + }, +} + +export default createForm()(Form) + diff --git a/components/vc-form/demo/validateFirst.js b/components/vc-form/demo/validateFirst.js new file mode 100644 index 000000000..184e5ef90 --- /dev/null +++ b/components/vc-form/demo/validateFirst.js @@ -0,0 +1,132 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import { regionStyle, errorStyle } from './styles' + +const Email = { + props: { + form: Object, + }, + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('email') + return (
+
email sync validate
+
+ +
+
+ {errors ? errors.join(',') : null} +
+
+ {isFieldValidating('email') ? 'validating' : null} +
+
) + }, + +} + +const User = { + props: { + form: Object, + }, + methods: { + userExists (rule, value, callback) { + setTimeout(() => { + if (value === '1') { + callback([new Error('are you kidding?')]) + } else if (value === 'yiminghe') { + callback([new Error('forbid yiminghe')]) + } else { + callback() + } + }, 300) + }, + }, + + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('user') + return (
+
user async validate
+
+ +
+
+ {(errors) ? errors.join(',') : null} +
+
+ {isFieldValidating('user') ? 'validating' : null} +
+
) + }, +} + +const Form = { + methods: { + onSubmit (e) { + console.log('submit') + e.preventDefault() + this.form.validateFields({ + // firstFields: false, + }, (error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + + reset (e) { + e.preventDefault() + this.form.resetFields() + }, + }, + + render () { + const { form } = this + return (
+

validateFirst

+
+ + + + +
+ +   + +
+ +
) + }, +} + +export default createForm()(Form) + diff --git a/components/vc-form/src/createBaseForm.jsx b/components/vc-form/src/createBaseForm.jsx index dd482bf80..d5016a94d 100644 --- a/components/vc-form/src/createBaseForm.jsx +++ b/components/vc-form/src/createBaseForm.jsx @@ -96,7 +96,7 @@ function createBaseForm (option = {}, mixins = []) { const valuesAllSet = {} valuesAll[name] = value Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key])) - onValuesChange(this.$props, set({}, name, value), valuesAllSet) + onValuesChange(this, set({}, name, value), valuesAllSet) } const field = this.fieldsStore.getField(name) return ({ name, field: { ...field, value, touched: true }, fieldMeta }) @@ -300,7 +300,7 @@ function createBaseForm (option = {}, mixins = []) { if (onFieldsChange) { const changedFields = Object.keys(fields) .reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {}) - onFieldsChange(this.$props, changedFields, this.fieldsStore.getNestedAllFields()) + onFieldsChange(this, changedFields, this.fieldsStore.getNestedAllFields()) } this.$forceUpdate() this.$nextTick(() => { @@ -344,7 +344,7 @@ function createBaseForm (option = {}, mixins = []) { this.setFields(newFields, callback) if (onValuesChange) { const allValues = this.fieldsStore.getAllValues() - onValuesChange(this.$props, changedValues, allValues) + onValuesChange(this, changedValues, allValues) } }, From 9c9b039421b2e8d65ef2c43d5428931606e76b05 Mon Sep 17 00:00:00 2001 From: tjz <415800467@qq.com> Date: Fri, 4 May 2018 22:41:32 +0800 Subject: [PATCH 17/39] feat: add vc-form demo --- components/vc-form/demo/validateTrigger.js | 112 +++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 components/vc-form/demo/validateTrigger.js diff --git a/components/vc-form/demo/validateTrigger.js b/components/vc-form/demo/validateTrigger.js new file mode 100644 index 000000000..3cf08b930 --- /dev/null +++ b/components/vc-form/demo/validateTrigger.js @@ -0,0 +1,112 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import { createForm } from '../index' +import { regionStyle, errorStyle } from './styles' + +const Email = { + props: { + form: Object, + }, + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('email') + return (
+
email validate onBlur && onChange
+
+ +
+
+ {errors ? errors.join(',') : null} +
+
+ {isFieldValidating('email') ? 'validating' : null} +
+
) + }, + +} + +const User = { + props: { + form: Object, + }, + render () { + const { getFieldProps, getFieldError, isFieldValidating } = this.form + const errors = getFieldError('user') + return (
+
user validate on submit
+
+ +
+
+ {(errors) ? errors.join(',') : null} +
+
+ {isFieldValidating('user') ? 'validating' : null} +
+
) + }, +} + +const Form = { + props: { + form: Object, + }, + methods: { + onSubmit (e) { + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + }, + + render () { + const { form } = this + return (
+

use validateTrigger config

+
+ + + + +
+ +
+ +
) + }, +} + +export default createForm()(Form) + From d1f017954e38a3ddf2b2514f66a5452e39795d89 Mon Sep 17 00:00:00 2001 From: tjz <415800467@qq.com> Date: Sat, 5 May 2018 17:00:51 +0800 Subject: [PATCH 18/39] feat: add form --- components/_util/props-util.js | 4 +- components/form/Form.jsx | 193 +++++++ components/form/FormItem.jsx | 333 ++++++++++++ components/form/constants.jsx | 2 + components/form/demo/test.vue | 65 +++ components/form/index.en-US.md | 181 +++++++ components/form/index.jsx | 6 + components/form/index.zh-CN.md | 182 +++++++ components/form/style/index.js | 5 + components/form/style/index.less | 624 ++++++++++++++++++++++ components/form/style/mixin.less | 106 ++++ components/index.js | 5 +- components/input/inputProps.js | 1 - components/style.js | 1 + components/vc-form/src/createBaseForm.jsx | 15 +- site/components.js | 5 +- site/routes.js | 2 +- 17 files changed, 1720 insertions(+), 10 deletions(-) create mode 100755 components/form/Form.jsx create mode 100644 components/form/FormItem.jsx create mode 100644 components/form/constants.jsx create mode 100644 components/form/demo/test.vue create mode 100644 components/form/index.en-US.md create mode 100644 components/form/index.jsx create mode 100644 components/form/index.zh-CN.md create mode 100644 components/form/style/index.js create mode 100644 components/form/style/index.less create mode 100644 components/form/style/mixin.less diff --git a/components/_util/props-util.js b/components/_util/props-util.js index 6211c7bc5..1b9394a53 100644 --- a/components/_util/props-util.js +++ b/components/_util/props-util.js @@ -40,9 +40,9 @@ const filterProps = (props, propsData = {}) => { return res } const getSlots = (ele) => { - let componentOptions = ele.componentOptions + let componentOptions = ele.componentOptions || {} if (ele.$vnode) { - componentOptions = ele.$vnode.componentOptions + componentOptions = ele.$vnode.componentOptions || {} } const children = componentOptions.children || [] const slots = {} diff --git a/components/form/Form.jsx b/components/form/Form.jsx new file mode 100755 index 000000000..63de4a8e3 --- /dev/null +++ b/components/form/Form.jsx @@ -0,0 +1,193 @@ +import PropTypes from '../_util/vue-types' +import classNames from 'classnames' +import isRegExp from 'lodash/isRegExp' +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 } from '../_util/props-util' + +export const FormCreateOption = { + onFieldsChange: PropTypes.func, + onValuesChange: PropTypes.func, + mapPropsToFields: PropTypes.func, + withRef: PropTypes.bool, +} + +// 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']), + form: PropTypes.shape(WrappedFormUtils).loose, + // onSubmit: React.FormEventHandler; + prefixCls: PropTypes.string, + hideRequiredMark: 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; +// /** 校验子节点值的时机 */ +// 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; +// }; + +export default { + name: 'AForm', + props: initDefaultProps(FormProps, { + prefixCls: 'ant-form', + layout: 'horizontal', + hideRequiredMark: false, + }), + // static defaultProps = { + // prefixCls: 'ant-form', + // layout: 'horizontal', + // hideRequiredMark: false, + // onSubmit (e) { + // e.preventDefault() + // }, + // }; + + // static propTypes = { + // prefixCls: PropTypes.string, + // layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), + // children: PropTypes.any, + // onSubmit: PropTypes.func, + // hideRequiredMark: PropTypes.bool, + // }; + + Item: FormItem, + + createFormField: createFormField, + + create: (options = {}) => { + return createDOMForm({ + fieldNameProp: 'id', + ...options, + fieldMetaProp: FIELD_META_PROP, + fieldDataProp: FIELD_DATA_PROP, + }) + }, + + // constructor (props) { + // super(props) + + // warning(!props.form, 'It is unnecessary to pass `form` to `Form` after antd@1.7.0.') + // } + + // shouldComponentUpdate(...args) { + // return PureRenderMixin.shouldComponentUpdate.apply(this, args); + // } + + // getChildContext () { + // const { layout } = this.props + // return { + // vertical: layout === 'vertical', + // } + // }, + provide () { + return { + FormProps: this.$props, + } + }, + methods: { + onSubmit (e) { + const { $listeners } = this + if (!$listeners.submit) { + e.preventDefault() + } else { + this.$emit('submit', e) + } + }, + }, + + render () { + const { + prefixCls, hideRequiredMark, layout, onSubmit, $slots, + } = this + + const formClassName = classNames(prefixCls, { + [`${prefixCls}-horizontal`]: layout === 'horizontal', + [`${prefixCls}-vertical`]: layout === 'vertical', + [`${prefixCls}-inline`]: layout === 'inline', + [`${prefixCls}-hide-required-mark`]: hideRequiredMark, + }) + + return
{$slots.default}
+ }, +} diff --git a/components/form/FormItem.jsx b/components/form/FormItem.jsx new file mode 100644 index 000000000..55af99cb2 --- /dev/null +++ b/components/form/FormItem.jsx @@ -0,0 +1,333 @@ + +import PropTypes from '../_util/vue-types' +import classNames from 'classnames' +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, getComponentFromProp, filterEmpty, getSlotOptions, getSlots } from '../_util/props-util' +import getTransitionProps from '../_util/getTransitionProps' +import BaseMixin from '../_util/BaseMixin' +export const FormItemProps = { + id: 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, +} + +export default { + name: 'AFormItem', + __ANT_FORM_ITEM: true, + mixins: [BaseMixin], + props: initDefaultProps(FormItemProps, { + hasFeedback: false, + prefixCls: 'ant-form', + colon: true, + }), + inject: { + FormProps: { default: {}}, + }, + data () { + return { helpShow: false } + }, + mounted () { + warning( + this.getControls(this.$slots.default, true).length <= 1, + '`Form.Item` cannot generate `validateStatus` and `help` automatically, ' + + 'while there are more than one `getFieldDecorator` in it.', + ) + }, + + // shouldComponentUpdate(...args: any[]) { + // return PureRenderMixin.shouldComponentUpdate.apply(this, args); + // } + methods: { + getHelpMsg () { + const help = getComponentFromProp(this, 'help') + const onlyControl = this.getOnlyControl() + if (help === undefined && onlyControl) { + const errors = this.getField().errors + return errors ? errors.map((e) => e.message).join(', ') : '' + } + + 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 (getSlotOptions(child).__ANT_FORM_ITEM) { + continue + } + const attrs = child.data && child.data.attrs + if (!attrs) { + continue + } + const slots = getSlots(child) + if (FIELD_META_PROP in attrs) { // And means FIELD_DATA_PROP in chidl.props, too. + controls.push(child) + } else if (slots.default) { + controls = controls.concat(this.getControls(slots.default, recursively)) + } + } + return controls + }, + + getOnlyControl () { + const child = this.getControls(this.$slots.default, false)[0] + return child !== undefined ? child : null + }, + + getChildAttr (prop) { + const child = this.getOnlyControl() + let data = {} + 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) + }, + + onHelpAnimEnd (_key, helpShow) { + this.setState({ helpShow }) + }, + + renderHelp () { + const prefixCls = this.prefixCls + const help = this.getHelpMsg() + const children = help ? ( +
+ {help} +
+ ) : null + const transitionProps = getTransitionProps('show-help', { + afterLeave: this.onHelpAnimEnd, + }) + return ( + + {children} + + ) + }, + + renderExtra () { + const { prefixCls } = this + const extra = getComponentFromProp(this, 'extra') + return extra ? ( +
{extra}
+ ) : null + }, + + 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 '' + }, + + renderValidateWrapper (c1, c2, c3) { + const props = this.$props + const onlyControl = this.getOnlyControl + const validateStatus = (props.validateStatus === undefined && onlyControl) + ? this.getValidateStatus() + : props.validateStatus + + let classes = `${props.prefixCls}-item-control` + if (validateStatus) { + classes = classNames(`${props.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', + }) + } + return ( +
+ {c1} + {c2}{c3} +
+ ) + }, + + renderWrapper (children) { + const { prefixCls, wrapperCol = {}} = this + const { class: cls, style, id, on, ...restProps } = wrapperCol + const className = classNames( + `${prefixCls}-item-control-wrapper`, + cls, + ) + const colProps = { + props: restProps, + class: className, + key: 'wrapper', + style, + id, + on, + } + return ( + + {children} + + ) + }, + + 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 + }, + + // Resolve duplicated ids bug between different forms + // https://github.com/ant-design/ant-design/issues/7351 + onLabelClick (e) { + const label = getComponentFromProp(this, 'label') + const id = this.id || this.getId() + if (!id) { + return + } + const controls = document.querySelectorAll(`[id="${id}"]`) + if (controls.length !== 1) { + // Only prevent in default situation + // Avoid preventing event in `label={link}`` + if (typeof label === 'string') { + e.preventDefault() + } + const control = this.$el.querySelector(`[id="${id}"]`) + if (control && control.focus) { + control.focus() + } + } + }, + + renderLabel () { + const { prefixCls, labelCol = {}, colon, id } = this + const label = getComponentFromProp(this, 'label') + const required = this.isRequired() + const { class: labelColClass, style: labelColStyle, id: labelColId, on, ...restProps } = labelCol + const labelColClassName = classNames( + `${prefixCls}-item-label`, + labelColClass, + ) + const labelClassName = classNames({ + [`${prefixCls}-item-required`]: required, + }) + + let labelChildren = label + // Keep label is original where there should have no colon + const haveColon = colon && this.FormProps.layout !== 'vertical' + // Remove duplicated user input colon + if (haveColon && typeof label === 'string' && label.trim() !== '') { + labelChildren = label.replace(/[:|:]\s*$/, '') + } + const colProps = { + props: restProps, + class: labelColClassName, + key: 'label', + style: labelColStyle, + id: labelColId, + on, + } + + return label ? ( + + + + ) : null + }, + renderChildren () { + const { $slots } = this + return [ + this.renderLabel(), + this.renderWrapper( + this.renderValidateWrapper( + filterEmpty($slots.default || []), + this.renderHelp(), + this.renderExtra(), + ), + ), + ] + }, + renderFormItem (children) { + const props = this.$props + const prefixCls = props.prefixCls + const itemClassName = { + [`${prefixCls}-item`]: true, + [`${prefixCls}-item-with-help`]: !!this.getHelpMsg() || this.helpShow, + [`${prefixCls}-item-no-colon`]: !props.colon, + } + + return ( + + {children} + + ) + }, + }, + + render () { + const children = this.renderChildren() + return this.renderFormItem(children) + }, +} diff --git a/components/form/constants.jsx b/components/form/constants.jsx new file mode 100644 index 000000000..4c84e31ce --- /dev/null +++ b/components/form/constants.jsx @@ -0,0 +1,2 @@ +export const FIELD_META_PROP = 'data-__meta' +export const FIELD_DATA_PROP = 'data-__field' diff --git a/components/form/demo/test.vue b/components/form/demo/test.vue new file mode 100644 index 000000000..11d110f9e --- /dev/null +++ b/components/form/demo/test.vue @@ -0,0 +1,65 @@ + + \ No newline at end of file diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md new file mode 100644 index 000000000..41504e7fb --- /dev/null +++ b/components/form/index.en-US.md @@ -0,0 +1,181 @@ +--- +category: Components +type: Data Entry +cols: 1 +title: Form +--- + +Form is used to collect, validate, and submit the user input, usually contains various form items including checkbox, radio, input, select, and etc. + +## Form + +You can align the controls of a `form` using the `layout` prop: + +- `horizontal`:to horizontally align the `label`s and controls of the fields. (Default) +- `vertical`:to vertically align the `label`s and controls of the fields. +- `inline`:to render form fields in one line. + +## Form fields + +A form consists of one or more form fields whose type includes input, textarea, checkbox, radio, select, tag, and more. +A form field is defined using ``. + +```jsx + + {children} + +``` + +## API + +### Form + +**more example [rc-form](http://react-component.github.io/form/)**。 + +| Property | Description | Type | Default Value | +| -------- | ----------- | ---- | ------------- | +| form | Decorated by `Form.create()` will be automatically set `this.props.form` property, so just pass to form, you don't need to set it by yourself after 1.7.0. | object | n/a | +| hideRequiredMark | Hide required mark of all form items | Boolean | false | +| layout | Define form layout(Support after 2.8) | 'horizontal'\|'vertical'\|'inline' | 'horizontal' | +| onSubmit | Defines a function will be called if form data validation is successful. | Function(e:Event) | | + +### Form.create(options) + +How to use: + +```jsx +class CustomizedForm extends React.Component {} + +CustomizedForm = Form.create({})(CustomizedForm); +``` + +The following `options` are available: + +| Property | Description | Type | +| -------- | ----------- | ---- | +| mapPropsToFields | Convert props to field value(e.g. reading the values from Redux store). And you must mark returned fields with [`Form.createFormField`](#Form.createFormField) | (props) => Object{ fieldName: FormField { value } } | +| validateMessages | Default validate message. And its format is similar with [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js)'s returned value | Object { [nested.path]: String } | +| onFieldsChange | Specify a function that will be called when the value a `Form.Item` gets changed. Usage example: saving the field's value to Redux store. | Function(props, fields) | +| onValuesChange | A handler while value of any field is changed | (props, values) => void | + +If the form has been decorated by `Form.create` then it has `this.props.form` property. `this.props.form` provides some APIs as follows: + +> Note: Before using `getFieldsValue` `getFieldValue` `setFieldsValue` and so on, please make sure that corresponding field had been registered with `getFieldDecorator`. + +| Method | Description | Type | +| ------ | ----------- | ---- | +| getFieldDecorator | Two-way binding for form, please read below for details. | | +| getFieldError | Get the error of a field. | Function(name) | +| getFieldsError | Get the specified fields' error. If you don't specify a parameter, you will get all fields' error. | Function(\[names: string\[]]) | +| getFieldsValue | Get the specified fields' values. If you don't specify a parameter, you will get all fields' values. | Function(\[fieldNames: string\[]]) | +| getFieldValue | Get the value of a field. | Function(fieldName: string) | +| isFieldsTouched | Check whether any of fields is touched by `getFieldDecorator`'s `options.trigger` event | (names?: string\[]) => boolean | +| isFieldTouched | Check whether a field is touched by `getFieldDecorator`'s `options.trigger` event | (name: string) => boolean | +| isFieldValidating | Check if the specified field is being validated. | Function(name) | +| resetFields | Reset the specified fields' value(to `initialValue`) and status. If you don't specify a parameter, all the fields will be reset. | Function(\[names: string\[]]) | +| setFields | Set the value and error of a field. [Code Sample](https://github.com/react-component/form/blob/3b9959b57ab30b41d8890ff30c79a7e7c383cad3/examples/server-validate.js#L74-L79) | Function({ [fieldName]: { value: any, errors: [Error] } }) | +| setFields | | Function(obj: object) | +| setFieldsValue | Set the value of a field.(Note: please don't use it in `componentWillReceiveProps`, otherwise, it will cause an endless loop, [more](https://github.com/ant-design/ant-design/issues/2985)) | Function({ [fieldName]: value } | +| validateFields | Validate the specified fields and get theirs values and errors. If you don't specify the parameter of fieldNames, you will vaildate all fields. | Function(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) | +| validateFieldsAndScroll | This function is similar to `validateFields`, but after validation, if the target field is not in visible area of form, form will be automatically scrolled to the target field area. | same as `validateFields` | + +### this.props.form.validateFields/validateFieldsAndScroll(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) + +| Method | Description | Type | Default | +| ------ | ----------- | ---- | ------- | +| options.first | If `true`, every field will stop validation at first failed rule | boolean | false | +| options.firstFields | Those fields will stop validation at first failed rule | String\[] | \[] | +| options.force | Should validate validated field again when `validateTrigger` is been triggered again | boolean | false | +| options.scroll | Config scroll behavior of `validateFieldsAndScroll`, more: [dom-scroll-into-view's config](https://github.com/yiminghe/dom-scroll-into-view#function-parameter) | Object | {} | + +### Form.createFormField + +To mark the returned fields data in `mapPropsToFields`, [demo](#components-form-demo-global-state). + +### this.props.form.getFieldDecorator(id, options) + +After wrapped by `getFieldDecorator`, `value`(or other property defined by `valuePropName`) `onChange`(or other property defined by `trigger`) props will be added to form controls,the flow of form data will be handled by Form which will cause: + +1. You shouldn't use `onChange` to collect data, but you still can listen to `onChange`(and so on) events. +2. You can not set value of form control via `value` `defaultValue` prop, and you should set default value with `initialValue` in `getFieldDecorator` instead. +3. You shouldn't call `setState` manually, please use `this.props.form.setFieldsValue` to change value programmatically. + +#### Special attention + +1. `getFieldDecorator` can not be used to decorate stateless component. +2. If you use `react@<15.3.0`, then, you can't use `getFieldDecorator` in stateless component: + +#### getFieldDecorator(id, options) parameters + +| Property | Description | Type | Default Value | +| -------- | ----------- | ---- | ------------- | +| id | The unique identifier is required. support [nested fields format](https://github.com/react-component/form/pull/48). | string | | +| options.getValueFromEvent | Specify how to get value from event or other onChange arguments | function(..args) | [reference](https://github.com/react-component/form#option-object) | +| options.initialValue | You can specify initial value, type, optional value of children node. (Note: Because `Form` will test equality with `===` internaly, we recommend to use vairable as `initialValue`, instead of literal) | | n/a | +| options.normalize | Normalize value to form component, [a select-all example](https://codepen.io/afc163/pen/JJVXzG?editors=001) | function(value, prevValue, allValues): any | - | +| options.rules | Includes validation rules. Please refer to "Validation Rules" part for details. | object\[] | n/a | +| options.trigger | When to collect the value of children node | string | 'onChange' | +| options.validateFirst | Whether stop validate on first rule of error for this field. | boolean | false | +| options.validateTrigger | When to validate the value of children node. | string\|string\[] | 'onChange' | +| options.valuePropName | Props of children node, for example, the prop of Switch is 'checked'. | string | 'value' | + +More option at [rc-form option](https://github.com/react-component/form#option-object)。 + +### Form.Item + +Note: + +- If Form.Item has multiple children that had been decorated by `getFieldDecorator`, `help` and `required` and `validateStatus` can't be generated automatically. +- Before `2.2.0`, form controls must be child of Form.Item, otherwise, you need to set `help`, `required` and `validateStatus` by yourself. + +| Property | Description | Type | Default Value | +| -------- | ----------- | ---- | ------------- | +| colon | Used with `label`, whether to display `:` after label text. | boolean | true | +| extra | The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time. | string\|ReactNode | | +| hasFeedback | Used with `validateStatus`, this option specifies the validation status icon. Recommended to be used only with `Input`. | boolean | false | +| help | The prompt message. If not provided, the prompt message will be generated by the validation rule. | string\|ReactNode | | +| label | Label text | string\|ReactNode | | +| labelCol | The layout of label. You can set `span` `offset` to something like `{span: 3, offset: 12}` or `sm: {span: 3, offset: 12}` same as with `` | [object](https://ant.design/components/grid/#Col) | | +| required | Whether provided or not, it will be generated by the validation rule. | boolean | false | +| validateStatus | The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating' | string | | +| wrapperCol | The layout for input controls, same as `labelCol` | [object](https://ant.design/components/grid/#Col) | | + +### Validation Rules + +| Property | Description | Type | Default Value | +| -------- | ----------- | ---- | ------------- | +| enum | validate a value from a list of possible values | string | - | +| len | validate an exact length of a field | number | - | +| max | validate a max length of a field | number | - | +| message | validation error message | string | - | +| min | validate a min length of a field | number | - | +| pattern | validate from a regular expression | RegExp | - | +| required | indicates whether field is required | boolean | `false` | +| transform | transform a value before validation | function(value) => transformedValue:any | - | +| type | built-in validation type, [available options](https://github.com/yiminghe/async-validator#type) | string | 'string' | +| validator | custom validate function (Note: [callback must be called](https://github.com/ant-design/ant-design/issues/5155)) | function(rule, value, callback) | - | +| whitespace | treat required fields that only contain whitespace as errors | boolean | `false` | + +See more advanced usage at [async-validator](https://github.com/yiminghe/async-validator). + + + +## Using in TypeScript + +```jsx +import { Form } from 'antd'; +import { FormComponentProps } from 'antd/lib/form'; + +interface UserFormProps extends FormComponentProps { + age: number; + name: string; +} + +class UserForm extends React.Component { + +} +``` diff --git a/components/form/index.jsx b/components/form/index.jsx new file mode 100644 index 000000000..07ea84244 --- /dev/null +++ b/components/form/index.jsx @@ -0,0 +1,6 @@ +import Form from './Form' + +export { FormProps, FormComponentProps, FormCreateOption, ValidateCallback, ValidationRule } from './Form' +export { FormItemProps } from './FormItem' + +export default Form diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md new file mode 100644 index 000000000..b0629f433 --- /dev/null +++ b/components/form/index.zh-CN.md @@ -0,0 +1,182 @@ +--- +category: Components +subtitle: 表单 +type: Data Entry +cols: 1 +title: Form +--- + +具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。 + +## 表单 + +我们为 `form` 提供了以下三种排列方式: + +- 水平排列:标签和表单控件水平排列;(默认) +- 垂直排列:标签和表单控件上下垂直排列; +- 行内排列:表单项水平行内排列。 + +## 表单域 + +表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等。 + +这里我们封装了表单域 `` 。 + +```jsx + + {children} + +``` + +## API + +### Form + +**更多示例参考 [rc-form](http://react-component.github.io/form/)**。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| form | 经 `Form.create()` 包装过的组件会自带 `this.props.form` 属性,直接传给 Form 即可。1.7.0 之后无需设置 | object | 无 | +| hideRequiredMark | 隐藏所有表单项的必选标记 | Boolean | false | +| layout | 表单布局(2.8 之后支持) | 'horizontal'\|'vertical'\|'inline' | 'horizontal' | +| onSubmit | 数据验证成功后回调事件 | Function(e:Event) | | + +### Form.create(options) + +使用方式如下: + +```jsx +class CustomizedForm extends React.Component {} + +CustomizedForm = Form.create({})(CustomizedForm); +``` + +`options` 的配置项如下。 + +| 参数 | 说明 | 类型 | +| --- | --- | --- | +| mapPropsToFields | 把父组件的属性映射到表单项上(如:把 Redux store 中的值读出),需要对返回值中的表单域数据用 [`Form.createFormField`](#Form.createFormField) 标记 | (props) => Object{ fieldName: FormField { value } } | +| validateMessages | 默认校验信息,可用于把默认错误信息改为中文等,格式与 [newMessages](https://github.com/yiminghe/async-validator/blob/master/src/messages.js) 返回值一致 | Object { [nested.path]: String } | +| onFieldsChange | 当 `Form.Item` 子节点的值发生改变时触发,可以把对应的值转存到 Redux store | Function(props, fields) | +| onValuesChange | 任一表单域的值发生改变时的回调 | (props, values) => void | + +经过 `Form.create` 包装的组件将会自带 `this.props.form` 属性,`this.props.form` 提供的 API 如下: + +> 注意:使用 `getFieldsValue` `getFieldValue` `setFieldsValue` 等时,应确保对应的 field 已经用 `getFieldDecorator` 注册过了。 + +| 方法      | 说明                                     | 类型       | +| ------- | -------------------------------------- | -------- | +| getFieldDecorator | 用于和表单进行双向绑定,详见下方描述 | | +| getFieldError | 获取某个输入控件的 Error | Function(name) | +| getFieldsError | 获取一组输入控件的 Error ,如不传入参数,则获取全部组件的 Error | Function(\[names: string\[]]) | +| getFieldsValue | 获取一组输入控件的值,如不传入参数,则获取全部组件的值 | Function(\[fieldNames: string\[]]) | +| getFieldValue | 获取一个输入控件的值 | Function(fieldName: string) | +| isFieldsTouched | 判断是否任一输入控件经历过 `getFieldDecorator` 的值收集时机 `options.trigger` | (names?: string\[]) => boolean | +| isFieldTouched | 判断一个输入控件是否经历过 `getFieldDecorator` 的值收集时机 `options.trigger` | (name: string) => boolean | +| isFieldValidating | 判断一个输入控件是否在校验状态 | Function(name) | +| resetFields | 重置一组输入控件的值(为 `initialValue`)与状态,如不传入参数,则重置所有组件 | Function(\[names: string\[]]) | +| setFields | 设置一组输入控件的值与 Error。 [代码](https://github.com/react-component/form/blob/3b9959b57ab30b41d8890ff30c79a7e7c383cad3/examples/server-validate.js#L74-L79) | Function({ [fieldName]: { value: any, errors: [Error] } }) | +| setFieldsValue | 设置一组输入控件的值(注意:不要在 `componentWillReceiveProps` 内使用,否则会导致死循环,[更多](https://github.com/ant-design/ant-design/issues/2985)) | Function({ [fieldName]: value } | +| validateFields | 校验并获取一组输入域的值与 Error,若 fieldNames 参数为空,则校验全部组件 | Function(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) | +| validateFieldsAndScroll | 与 `validateFields` 相似,但校验完后,如果校验不通过的菜单域不在可见范围内,则自动滚动进可见范围 | 参考 `validateFields` | + +### this.props.form.validateFields/validateFieldsAndScroll(\[fieldNames: string\[]], [options: object], callback: Function(errors, values)) + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| options.first | 若为 true,则每一表单域的都会在碰到第一个失败了的校验规则后停止校验 | boolean | false | +| options.firstFields | 指定表单域会在碰到第一个失败了的校验规则后停止校验 | String\[] | \[] | +| options.force | 对已经校验过的表单域,在 validateTrigger 再次被触发时是否再次校验 | boolean | false | +| options.scroll | 定义 validateFieldsAndScroll 的滚动行为,详细配置见 [dom-scroll-into-view config](https://github.com/yiminghe/dom-scroll-into-view#function-parameter) | Object | {} | + +### Form.createFormField + +用于标记 `mapPropsToFields` 返回的表单域数据,[例子](#components-form-demo-global-state)。 + +### this.props.form.getFieldDecorator(id, options) + +经过 `getFieldDecorator` 包装的控件,表单控件会自动添加 `value`(或 `valuePropName` 指定的其他属性) `onChange`(或 `trigger` 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果: + +1. 你**不再需要也不应该**用 `onChange` 来做同步,但还是可以继续监听 `onChange` 等事件。 +2. 你不能用控件的 `value` `defaultValue` 等属性来设置表单域的值,默认值可以用 `getFieldDecorator` 里的 `initialValue`。 +3. 你不应该用 `setState`,可以使用 `this.props.form.setFieldsValue` 来动态改变表单值。 + +#### 特别注意 + +1. `getFieldDecorator` 不能用于装饰纯函数组件。 +2. 如果使用的是 `react@<15.3.0`,则 `getFieldDecorator` 调用不能位于纯函数组件中: + +#### getFieldDecorator(id, options) 参数 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| id | 必填输入控件唯一标志。支持嵌套式的[写法](https://github.com/react-component/form/pull/48)。 | string | | +| options.getValueFromEvent | 可以把 onChange 的参数(如 event)转化为控件的值 | function(..args) | [reference](https://github.com/react-component/form#option-object) | +| options.initialValue | 子节点的初始值,类型、可选值均由子节点决定(注意:由于内部校验时使用 `===` 判断是否变化,建议使用变量缓存所需设置的值而非直接使用字面量)) | | | +| options.normalize | 转换默认的 value 给控件,[一个选择全部的例子](https://codepen.io/afc163/pen/JJVXzG?editors=001) | function(value, prevValue, allValues): any | - | +| options.rules | 校验规则,参考下方文档 | object\[] | | +| options.trigger | 收集子节点的值的时机 | string | 'onChange' | +| options.validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验 | boolean | false | +| options.validateTrigger | 校验子节点值的时机 | string\|string\[] | 'onChange' | +| options.valuePropName | 子节点的值的属性,如 Switch 的是 'checked' | string | 'value' | + +更多参数请查看 [rc-form option](https://github.com/react-component/form#option-object)。 + +### Form.Item + +注意: + +- 一个 Form.Item 建议只放一个被 getFieldDecorator 装饰过的 child,当有多个被装饰过的 child 时,`help` `required` `validateStatus` 无法自动生成。 +- `2.2.0` 之前,只有当表单域为 Form.Item 的子元素时,才会自动生成 `help` `required` `validateStatus`,嵌套情况需要自行设置。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| colon | 配合 label 属性使用,表示是否显示 label 后面的冒号 | boolean | true | +| extra | 额外的提示信息,和 help 类似,当需要错误信息和提示文案同时出现时,可以使用这个。 | string\|ReactNode | | +| hasFeedback | 配合 validateStatus 属性使用,展示校验状态图标,建议只配合 Input 组件使用 | boolean | false | +| help | 提示信息,如不设置,则会根据校验规则自动生成 | string\|ReactNode | | +| label | label 标签的文本 | string\|ReactNode | | +| labelCol | label 标签布局,同 `` 组件,设置 `span` `offset` 值,如 `{span: 3, offset: 12}` 或 `sm: {span: 3, offset: 12}` | [object](https://ant.design/components/grid/#Col) | | +| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | false | +| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | | +| wrapperCol | 需要为输入控件设置布局样式时,使用该属性,用法同 labelCol | [object](https://ant.design/components/grid/#Col) | | + +### 校验规则 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| enum | 枚举类型 | string | - | +| len | 字段长度 | number | - | +| max | 最大长度 | number | - | +| message | 校验文案 | string | - | +| min | 最小长度 | number | - | +| pattern | 正则表达式校验 | RegExp | - | +| required | 是否必选 | boolean | `false` | +| transform | 校验前转换字段值 | function(value) => transformedValue:any | - | +| type | 内建校验类型,[可选项](https://github.com/yiminghe/async-validator#type) | string | 'string' | +| validator | 自定义校验(注意,[callback 必须被调用](https://github.com/ant-design/ant-design/issues/5155)) | function(rule, value, callback) | - | +| whitespace | 必选时,空格是否会被视为错误 | boolean | `false` | + +更多高级用法可研究 [async-validator](https://github.com/yiminghe/async-validator)。 + + + +## 在 TypeScript 中使用 + +```jsx +import { Form } from 'antd'; +import { FormComponentProps } from 'antd/lib/form'; + +interface UserFormProps extends FormComponentProps { + age: number; + name: string; +} + +class UserForm extends React.Component { + +} +``` diff --git a/components/form/style/index.js b/components/form/style/index.js new file mode 100644 index 000000000..753451010 --- /dev/null +++ b/components/form/style/index.js @@ -0,0 +1,5 @@ +import '../../style/index.less' +import './index.less' + +// style dependencies +import '../../grid/style' diff --git a/components/form/style/index.less b/components/form/style/index.less new file mode 100644 index 000000000..a26e07c2c --- /dev/null +++ b/components/form/style/index.less @@ -0,0 +1,624 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; +@import "../../input/style/mixin"; +@import "../../button/style/mixin"; +@import "../../grid/style/mixin"; +@import "./mixin"; + +@form-prefix-cls: ~"@{ant-prefix}-form"; +@form-component-height: @input-height-base; +@form-component-max-height: @input-height-lg; +@form-feedback-icon-size: 14px; +@form-help-margin-top: (@form-component-height - @form-component-max-height) / 2 + 2px; + +.@{form-prefix-cls} { + .reset-component; + .reset-form; +} + +.@{form-prefix-cls}-item-required:before { + display: inline-block; + margin-right: 4px; + content: "*"; + font-family: SimSun; + line-height: 1; + font-size: @font-size-base; + color: @label-required-color; + .@{form-prefix-cls}-hide-required-mark & { + display: none; + } +} + +// Radio && Checkbox +input[type="radio"], +input[type="checkbox"] { + &[disabled], + &.disabled { + cursor: not-allowed; + } +} + +// These classes are used directly on