diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 28762c48d..a61183c94 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -19,6 +19,7 @@ - 🔥🔥🔥 [Descriptions](https://antdv.com/components/descriptions/) Display multiple read-only fields in groups. - 🔥🔥🔥 [PageHeader](https://antdv.com/components/page-header/) can be used to declare the topic of the page, display important information about the page that the user is concerned about, and carry the operation items related to the current page. - 🔥🔥🔥 [Result](https://antdv.com/components/result) is used to feedback the processing results of a series of operation tasks. + - 🔥🔥🔥 [NewForm](https://antdv.com/components/n-form) Form components that use v-model for automatic validation are more concise than v-decorator forms. - 🔥 Descriptions supports vertical layout. - 🔥 Progress.Circle supports gradient colors. - 🔥 Progress.Line supports gradient colors. diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 1639a4c9b..08163a110 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -14,11 +14,12 @@ `2020-03-06` -- 新增了四个组件: +- 新增了五个组件: - 🔥🔥🔥 [Mentions](https://antdv.com/components/mentions-cn/) 新增提及组件并废弃原有 Mention 组件。 - 🔥🔥🔥 [Descriptions](https://antdv.com/components/descriptions-cn/) 成组展示多个只读字段。 - 🔥🔥🔥 [PageHeader](https://antdv.com/components/page-header-cn/) 可用于声明页面主题、展示用户所关注的页面重要信息,以及承载与当前页相关的操作项。 - 🔥🔥🔥 [Result](https://antdv.com/components/result) 用于反馈一系列操作任务的处理结果。 + - 🔥🔥🔥 [NewForm](https://antdv.com/components/n-form) 使用 v-model 进行自动校验的表单组件,相较于 v-decorator 形式的表单,更加简洁。 - 🔥 Descriptions 支持垂直布局。 - 🔥 Progress.Circle 支持渐变色。 - 🔥 Progress.Line 支持渐变色。 diff --git a/antdv-demo b/antdv-demo index 9a67d0070..3ad62e059 160000 --- a/antdv-demo +++ b/antdv-demo @@ -1 +1 @@ -Subproject commit 9a67d0070ac57b29c70624e8a630e39146b44725 +Subproject commit 3ad62e059028e716305b9d88ca1bbf9e4d79118c diff --git a/components/form/__tests__/__snapshots__/demo.test.js.snap b/components/form/__tests__/__snapshots__/demo.test.js.snap index 309295208..de4a124cb 100644 --- a/components/form/__tests__/__snapshots__/demo.test.js.snap +++ b/components/form/__tests__/__snapshots__/demo.test.js.snap @@ -277,47 +277,45 @@ exports[`renders ./antdv-demo/docs/form/demo/horizontal-login.vue correctly 1`] `; exports[`renders ./antdv-demo/docs/form/demo/layout.vue correctly 1`] = ` -
-
-
-
-
-
+ +
+
+
+
+ +
+
+
+
+
+
+
-
-
-
-
-
- +
+
+
+
+
+ +
-
-
-
-
-
- +
+
+
+ +
-
-
-
-
- -
-
-
-
`; exports[`renders ./antdv-demo/docs/form/demo/normal-login.vue correctly 1`] = ` diff --git a/components/n-form/Form.jsx b/components/n-form/Form.jsx index e46597e55..2e2c15edc 100755 --- a/components/n-form/Form.jsx +++ b/components/n-form/Form.jsx @@ -1,16 +1,11 @@ import PropTypes from '../_util/vue-types'; import classNames from 'classnames'; import { ColProps } from '../grid/Col'; -import Vue from 'vue'; import isRegExp from 'lodash/isRegExp'; import warning from '../_util/warning'; -import createDOMForm from '../vc-form/src/createDOMForm'; -import createFormField from '../vc-form/src/createFormField'; import FormItem from './FormItem'; -import { FIELD_META_PROP, FIELD_DATA_PROP } from './constants'; import { initDefaultProps, getListeners } from '../_util/props-util'; import { ConfigConsumerProps } from '../config-provider'; -import Base from '../base'; export const FormProps = { layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), @@ -144,7 +139,7 @@ const Form = { if (message) { valid = false; } - invalidFields = objectAssign({}, invalidFields, field); + invalidFields = Object.assign({}, invalidFields, field); if (typeof callback === 'function' && ++count === this.fields.length) { callback(valid, invalidFields); } diff --git a/components/n-form/FormItem.jsx b/components/n-form/FormItem.jsx index cec3f6f8a..fd293def6 100644 --- a/components/n-form/FormItem.jsx +++ b/components/n-form/FormItem.jsx @@ -1,10 +1,19 @@ import AsyncValidator from 'async-validator'; +import cloneDeep from 'lodash/cloneDeep'; import PropTypes from '../_util/vue-types'; import { ColProps } from '../grid/Col'; -import { initDefaultProps, getComponentFromProp, getOptionProps } from '../_util/props-util'; +import { + initDefaultProps, + getComponentFromProp, + getOptionProps, + getEvents, + filterEmpty, + isValidElement, +} from '../_util/props-util'; import BaseMixin from '../_util/BaseMixin'; import { ConfigConsumerProps } from '../config-provider'; import FormItem from '../form/FormItem'; +import { cloneElement } from '../_util/vnode'; function noop() {} @@ -45,8 +54,9 @@ export const FormItemProps = { hasFeedback: PropTypes.bool, colon: PropTypes.bool, labelAlign: PropTypes.oneOf(['left', 'right']), - name: PropTypes.string, - rules: PropTypes.array, + prop: PropTypes.string, + rules: PropTypes.oneOfType([Array, Object]), + autoLink: PropTypes.bool, required: PropTypes.bool, validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']), }; @@ -57,6 +67,7 @@ export default { mixins: [BaseMixin], props: initDefaultProps(FormItemProps, { hasFeedback: false, + autoLink: true, }), provide() { return { @@ -88,6 +99,20 @@ export default { } return getPropByPath(model, path, true).v; }, + isRequired() { + let rules = this.getRules(); + let isRequired = false; + if (rules && rules.length) { + rules.every(rule => { + if (rule.required) { + isRequired = true; + return false; + } + return true; + }); + } + return isRequired; + }, }, watch: { validateStatus(val) { @@ -98,11 +123,7 @@ export default { if (this.prop) { const { addField } = this.FormContext; addField && addField(this); - let initialValue = this.fieldValue; - if (Array.isArray(initialValue)) { - initialValue = [...initialValue]; - } - this.initialValue = initialValue; + this.initialValue = cloneDeep(this.fieldValue); } }, beforeDestroy() { @@ -113,7 +134,7 @@ export default { validate(trigger, callback = noop) { this.validateDisabled = false; const rules = this.getFilteredRule(trigger); - if ((!rules || rules.length === 0) && this.required === undefined) { + if (!rules || rules.length === 0) { callback(); return true; } @@ -138,12 +159,13 @@ export default { }); }, getRules() { - let formRules = this.FormContext.rules || {}; + let formRules = this.FormContext.rules; const selfRules = this.rules; - const requiredRule = this.required !== undefined ? { required: !!this.required } : []; + const requiredRule = + this.required !== undefined ? { required: !!this.required, trigger: 'change' } : []; const prop = getPropByPath(formRules, this.prop || ''); formRules = formRules ? prop.o[this.prop || ''] || prop.v : []; - return [...selfRules, ...formRules, ...requiredRule]; + return [].concat(selfRules || formRules || []).concat(requiredRule); }, getFilteredRule(trigger) { const rules = this.getRules(); @@ -196,7 +218,7 @@ export default { }, }, render() { - const { $slots } = this; + const { $slots, $scopedSlots } = this; const props = getOptionProps(this); const label = getComponentFromProp(this, 'label'); const extra = getComponentFromProp(this, 'extra'); @@ -208,8 +230,31 @@ export default { extra, validateStatus: this.validateState, help: this.validateMessage || help, + required: this.isRequired || props.required, }, }; - return {$slots.default}; + const children = filterEmpty($scopedSlots.default ? $scopedSlots.default() : $slots.default); + let firstChildren = children[0]; + if (this.prop && this.autoLink && isValidElement(firstChildren)) { + const originalEvents = getEvents(firstChildren); + firstChildren = cloneElement(firstChildren, { + on: { + blur: (...args) => { + originalEvents.blur && originalEvents.blur(...args); + this.onFieldBlur(); + }, + change: (...args) => { + originalEvents.change && originalEvents.change(...args); + this.onFieldChange(); + }, + }, + }); + } + return ( + + {firstChildren} + {children.slice(1)} + + ); }, }; diff --git a/components/n-form/__tests__/__snapshots__/demo.test.js.snap b/components/n-form/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 000000000..94c017595 --- /dev/null +++ b/components/n-form/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,282 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./antdv-demo/docs/n-form/demo/basic.md correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
please select your zone
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/n-form/demo/custom-validation.md correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/n-form/demo/dynamic-form-item.md correctly 1`] = ` +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/n-form/demo/horizontal-login.md correctly 1`] = ` +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/n-form/demo/layout.md correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; + +exports[`renders ./antdv-demo/docs/n-form/demo/validation.md correctly 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
please select your zone
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+`; diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index 1627daea6..7e20e436b 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -27,6 +27,7 @@ Array [ "Divider", "Dropdown", "Form", + "NewForm", "Icon", "Input", "InputNumber", diff --git a/types/n-form/form-item.d.ts b/types/n-form/form-item.d.ts new file mode 100644 index 000000000..0120457ee --- /dev/null +++ b/types/n-form/form-item.d.ts @@ -0,0 +1,69 @@ +// Project: https://github.com/vueComponent/ant-design-vue +// Definitions by: akki-jat +// Definitions: https://github.com/vueComponent/ant-design-vue/types + +import { AntdComponent } from '../component'; +import { Col } from '../grid/col'; + +export declare class NFormItem extends AntdComponent { + /** + * Used with label, whether to display : after label text. + * @default true + * @type boolean + */ + colon: boolean; + + /** + * The extra prompt message. It is similar to help. Usage example: to display error message and prompt message at the same time. + * @type any (string | slot) + */ + extra: any; + + /** + * Used with validateStatus, this option specifies the validation status icon. Recommended to be used only with Input. + * @default false + * @type boolean + */ + hasFeedback: boolean; + + /** + * The prompt message. If not provided, the prompt message will be generated by the validation rule. + * @type any (string | slot) + */ + help: any; + + /** + * Label test + * @type any (string | slot) + */ + label: any; + + /** + * 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 + * @type Col + */ + labelCol: Col; + + /** + * Whether provided or not, it will be generated by the validation rule. + * @default false + * @type boolean + */ + required: boolean; + + /** + * The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating' + * @type string + */ + validateStatus: '' | 'success' | 'warning' | 'error' | 'validating'; + + /** + * The layout for input controls, same as labelCol + * @type Col + */ + wrapperCol: Col; + labelAlign: 'left' | 'right'; + prop: string; + rules: object | array; + autoLink: boolean; +} diff --git a/types/n-form/form.d.ts b/types/n-form/form.d.ts new file mode 100644 index 000000000..517184ecb --- /dev/null +++ b/types/n-form/form.d.ts @@ -0,0 +1,115 @@ +// Project: https://github.com/vueComponent/ant-design-vue +// Definitions by: akki-jat +// Definitions: https://github.com/vueComponent/ant-design-vue/types + +import { AntdComponent } from '../component'; +import { Col } from '../grid/col'; +import Vue from 'vue'; +import { NFormItem } from './form-item'; + +declare interface ValidationRule { + trigger?: string; + /** + * validation error message + * @type string + */ + message?: string; + + /** + * built-in validation type, available options: https://github.com/yiminghe/async-validator#type + * @default 'string' + * @type string + */ + type?: string; + + /** + * indicates whether field is required + * @default false + * @type boolean + */ + required?: boolean; + + /** + * treat required fields that only contain whitespace as errors + * @default false + * @type boolean + */ + whitespace?: boolean; + + /** + * validate the exact length of a field + * @type number + */ + len?: number; + + /** + * validate the min length of a field + * @type number + */ + min?: number; + + /** + * validate the max length of a field + * @type number + */ + max?: number; + + /** + * validate the value from a list of possible values + * @type string | string[] + */ + enum?: string | string[]; + + /** + * validate from a regular expression + * @type boolean + */ + pattern?: RegExp; + + /** + * transform a value before validation + * @type Function + */ + transform?: (value: any) => any; + + /** + * custom validate function (Note: callback must be called) + * @type Function + */ + validator?: (rule: any, value: any, callback: Function) => any; +} + +export declare class NForm extends AntdComponent { + static Item: typeof NFormItem; + + /** + * Hide required mark of all form items + * @default false + * @type boolean + */ + hideRequiredMark: boolean; + + /** + * 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 + * @type Col + */ + labelCol: Col; + + /** + * Define form layout + * @default 'horizontal' + * @type string + */ + layout: 'horizontal' | 'inline' | 'vertical'; + + /** + * The layout for input controls, same as labelCol + * @type Col + */ + wrapperCol: Col; + colon: boolean; + labelAlign: 'left' | 'right'; + model: object; + rules: object; + validateOnRuleChange: boolean; +}