diff --git a/antdv-demo b/antdv-demo
index ce10f4f24..9a67d0070 160000
--- a/antdv-demo
+++ b/antdv-demo
@@ -1 +1 @@
-Subproject commit ce10f4f242cdf252dd25df905bada3b5f7f722e2
+Subproject commit 9a67d0070ac57b29c70624e8a630e39146b44725
diff --git a/components/form/Form.jsx b/components/form/Form.jsx
index 8441dc03e..d956d549d 100755
--- a/components/form/Form.jsx
+++ b/components/form/Form.jsx
@@ -151,7 +151,7 @@ const Form = {
},
provide() {
return {
- FormContextProps: this,
+ FormContext: this,
// https://github.com/vueComponent/ant-design-vue/issues/446
collectFormItemContext:
this.form && this.form.templateContext
diff --git a/components/form/FormItem.jsx b/components/form/FormItem.jsx
index 8731de351..18e96ff48 100644
--- a/components/form/FormItem.jsx
+++ b/components/form/FormItem.jsx
@@ -75,7 +75,7 @@ export default {
},
inject: {
isFormItemChildren: { default: false },
- FormContextProps: { default: () => ({}) },
+ FormContext: { default: () => ({}) },
decoratorFormProps: { default: () => ({}) },
collectFormItemContext: { default: () => noop },
configProvider: { default: () => ConfigConsumerProps },
@@ -85,7 +85,7 @@ export default {
},
computed: {
itemSelfUpdate() {
- return !!(this.selfUpdate === undefined ? this.FormContextProps.selfUpdate : this.selfUpdate);
+ return !!(this.selfUpdate === undefined ? this.FormContext.selfUpdate : this.selfUpdate);
},
},
created() {
@@ -117,8 +117,8 @@ export default {
},
methods: {
collectContext() {
- if (this.FormContextProps.form && this.FormContextProps.form.templateContext) {
- const { templateContext } = this.FormContextProps.form;
+ if (this.FormContext.form && this.FormContext.form.templateContext) {
+ const { templateContext } = this.FormContext.form;
const vnodes = Object.values(templateContext.$slots || {}).reduce((a, b) => {
return [...a, ...b];
}, []);
@@ -356,9 +356,7 @@ export default {
},
renderWrapper(prefixCls, children) {
- const { wrapperCol: contextWrapperCol } = this.isFormItemChildren
- ? {}
- : this.FormContextProps;
+ const { wrapperCol: contextWrapperCol } = this.isFormItemChildren ? {} : this.FormContext;
const { wrapperCol } = this;
const mergedWrapperCol = wrapperCol || contextWrapperCol || {};
const { style, id, on, ...restProps } = mergedWrapperCol;
@@ -380,7 +378,7 @@ export default {
labelAlign: contextLabelAlign,
labelCol: contextLabelCol,
colon: contextColon,
- } = this.FormContextProps;
+ } = this.FormContext;
const { labelAlign, labelCol, colon, id, htmlFor } = this;
const label = getComponentFromProp(this, 'label');
const required = this.isRequired();
@@ -481,8 +479,8 @@ export default {
}
},
decoratorChildren(vnodes) {
- const { FormContextProps } = this;
- const getFieldDecorator = FormContextProps.form.getFieldDecorator;
+ const { FormContext } = this;
+ const getFieldDecorator = FormContext.form.getFieldDecorator;
for (let i = 0, len = vnodes.length; i < len; i++) {
const vnode = vnodes[i];
if (getSlotOptions(vnode).__ANT_FORM_ITEM) {
@@ -510,7 +508,7 @@ export default {
decoratorFormProps,
fieldDecoratorId,
fieldDecoratorOptions = {},
- FormContextProps,
+ FormContext,
} = this;
let child = filterEmpty($slots.default || []);
if (decoratorFormProps.form && fieldDecoratorId && child.length) {
@@ -522,7 +520,7 @@ export default {
'`autoFormCreate` just `decorator` then first children. but you can use JSX to support multiple children',
);
this.slotDefault = child;
- } else if (FormContextProps.form) {
+ } else if (FormContext.form) {
child = cloneVNodes(child);
this.slotDefault = this.decoratorChildren(child);
} else {
diff --git a/components/index.js b/components/index.js
index dc581a4e6..5a1f03384 100644
--- a/components/index.js
+++ b/components/index.js
@@ -56,6 +56,7 @@ import { default as Divider } from './divider';
import { default as Dropdown } from './dropdown';
import { default as Form } from './form';
+import { default as NewForm } from './n-form';
import { default as Icon } from './icon';
@@ -166,6 +167,7 @@ const components = [
Divider,
Dropdown,
Form,
+ NewForm,
Icon,
Input,
InputNumber,
@@ -254,6 +256,7 @@ export {
Divider,
Dropdown,
Form,
+ NewForm,
Icon,
Input,
InputNumber,
diff --git a/components/input/Input.jsx b/components/input/Input.jsx
index 4b82589a1..f852224f5 100644
--- a/components/input/Input.jsx
+++ b/components/input/Input.jsx
@@ -161,6 +161,7 @@ export default {
this.removePasswordTimeout = setTimeout(() => {
if (
this.$refs.input &&
+ this.$refs.input.getAttribute &&
this.$refs.input.getAttribute('type') === 'password' &&
this.$refs.input.hasAttribute('value')
) {
diff --git a/components/n-form/Form.jsx b/components/n-form/Form.jsx
new file mode 100755
index 000000000..e46597e55
--- /dev/null
+++ b/components/n-form/Form.jsx
@@ -0,0 +1,189 @@
+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']),
+ labelCol: PropTypes.shape(ColProps).loose,
+ wrapperCol: PropTypes.shape(ColProps).loose,
+ colon: PropTypes.bool,
+ labelAlign: PropTypes.oneOf(['left', 'right']),
+ prefixCls: PropTypes.string,
+ hideRequiredMark: PropTypes.bool,
+ model: PropTypes.object,
+ rules: PropTypes.object,
+ validateOnRuleChange: 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,
+};
+
+const Form = {
+ name: 'ANForm',
+ props: initDefaultProps(FormProps, {
+ layout: 'horizontal',
+ hideRequiredMark: false,
+ colon: true,
+ }),
+ Item: FormItem,
+ created() {
+ this.fields = [];
+ },
+ provide() {
+ return {
+ FormContext: this,
+ };
+ },
+ inject: {
+ configProvider: { default: () => ConfigConsumerProps },
+ },
+ watch: {
+ rules() {
+ if (this.validateOnRuleChange) {
+ this.validate(() => {});
+ }
+ },
+ },
+ computed: {
+ vertical() {
+ return this.layout === 'vertical';
+ },
+ },
+ methods: {
+ addField(field) {
+ if (field) {
+ this.fields.push(field);
+ }
+ },
+ removeField(field) {
+ if (field.prop) {
+ this.fields.splice(this.fields.indexOf(field), 1);
+ }
+ },
+ onSubmit(e) {
+ if (!getListeners(this).submit) {
+ e.preventDefault();
+ } else {
+ this.$emit('submit', e);
+ }
+ },
+ resetFields() {
+ if (!this.model) {
+ warning(false, 'NewForm', 'model is required for resetFields to work.');
+ return;
+ }
+ this.fields.forEach(field => {
+ field.resetField();
+ });
+ },
+ clearValidate(props = []) {
+ const fields = props.length
+ ? typeof props === 'string'
+ ? this.fields.filter(field => props === field.prop)
+ : this.fields.filter(field => props.indexOf(field.prop) > -1)
+ : this.fields;
+ fields.forEach(field => {
+ field.clearValidate();
+ });
+ },
+ validate(callback) {
+ if (!this.model) {
+ warning(false, 'NewForm', 'model is required for resetFields to work.');
+ return;
+ }
+ let promise;
+ // if no callback, return promise
+ if (typeof callback !== 'function' && window.Promise) {
+ promise = new window.Promise((resolve, reject) => {
+ callback = function(valid) {
+ valid ? resolve(valid) : reject(valid);
+ };
+ });
+ }
+ let valid = true;
+ let count = 0;
+ // 如果需要验证的fields为空,调用验证时立刻返回callback
+ if (this.fields.length === 0 && callback) {
+ callback(true);
+ }
+ let invalidFields = {};
+ this.fields.forEach(field => {
+ field.validate('', (message, field) => {
+ if (message) {
+ valid = false;
+ }
+ invalidFields = objectAssign({}, invalidFields, field);
+ if (typeof callback === 'function' && ++count === this.fields.length) {
+ callback(valid, invalidFields);
+ }
+ });
+ });
+ if (promise) {
+ return promise;
+ }
+ },
+ validateField(props, cb) {
+ props = [].concat(props);
+ const fields = this.fields.filter(field => props.indexOf(field.prop) !== -1);
+ if (!fields.length) {
+ warning(false, 'NewForm', 'please pass correct props!');
+ return;
+ }
+ fields.forEach(field => {
+ field.validate('', cb);
+ });
+ },
+ },
+
+ render() {
+ const { prefixCls: customizePrefixCls, hideRequiredMark, layout, onSubmit, $slots } = this;
+ const getPrefixCls = this.configProvider.getPrefixCls;
+ const prefixCls = getPrefixCls('form', customizePrefixCls);
+
+ const formClassName = classNames(prefixCls, {
+ [`${prefixCls}-horizontal`]: layout === 'horizontal',
+ [`${prefixCls}-vertical`]: layout === 'vertical',
+ [`${prefixCls}-inline`]: layout === 'inline',
+ [`${prefixCls}-hide-required-mark`]: hideRequiredMark,
+ });
+ return (
+
+ );
+ },
+};
+
+export default Form;
diff --git a/components/n-form/FormItem.jsx b/components/n-form/FormItem.jsx
new file mode 100644
index 000000000..cec3f6f8a
--- /dev/null
+++ b/components/n-form/FormItem.jsx
@@ -0,0 +1,215 @@
+import AsyncValidator from 'async-validator';
+import PropTypes from '../_util/vue-types';
+import { ColProps } from '../grid/Col';
+import { initDefaultProps, getComponentFromProp, getOptionProps } from '../_util/props-util';
+import BaseMixin from '../_util/BaseMixin';
+import { ConfigConsumerProps } from '../config-provider';
+import FormItem from '../form/FormItem';
+
+function noop() {}
+
+function getPropByPath(obj, path, strict) {
+ let tempObj = obj;
+ path = path.replace(/\[(\w+)\]/g, '.$1');
+ path = path.replace(/^\./, '');
+
+ let keyArr = path.split('.');
+ let i = 0;
+ for (let len = keyArr.length; i < len - 1; ++i) {
+ if (!tempObj && !strict) break;
+ let key = keyArr[i];
+ if (key in tempObj) {
+ tempObj = tempObj[key];
+ } else {
+ if (strict) {
+ throw new Error('please transfer a valid prop path to form item!');
+ }
+ break;
+ }
+ }
+ return {
+ o: tempObj,
+ k: keyArr[i],
+ v: tempObj ? tempObj[keyArr[i]] : null,
+ };
+}
+export const FormItemProps = {
+ id: PropTypes.string,
+ htmlFor: PropTypes.string,
+ prefixCls: PropTypes.string,
+ label: PropTypes.any,
+ help: PropTypes.any,
+ extra: PropTypes.any,
+ labelCol: PropTypes.shape(ColProps).loose,
+ wrapperCol: PropTypes.shape(ColProps).loose,
+ hasFeedback: PropTypes.bool,
+ colon: PropTypes.bool,
+ labelAlign: PropTypes.oneOf(['left', 'right']),
+ name: PropTypes.string,
+ rules: PropTypes.array,
+ required: PropTypes.bool,
+ validateStatus: PropTypes.oneOf(['', 'success', 'warning', 'error', 'validating']),
+};
+
+export default {
+ name: 'ANFormItem',
+ __ANT_NEW_FORM_ITEM: true,
+ mixins: [BaseMixin],
+ props: initDefaultProps(FormItemProps, {
+ hasFeedback: false,
+ }),
+ provide() {
+ return {
+ NewFormItemContext: this,
+ };
+ },
+ inject: {
+ configProvider: { default: () => ConfigConsumerProps },
+ FormContext: { default: () => ({}) },
+ },
+ data() {
+ return {
+ validateState: this.validateStatus,
+ validateMessage: '',
+ validateDisabled: false,
+ validator: {},
+ };
+ },
+
+ computed: {
+ fieldValue() {
+ const model = this.FormContext.model;
+ if (!model || !this.prop) {
+ return;
+ }
+ let path = this.prop;
+ if (path.indexOf(':') !== -1) {
+ path = path.replace(/:/g, '.');
+ }
+ return getPropByPath(model, path, true).v;
+ },
+ },
+ watch: {
+ validateStatus(val) {
+ this.validateState = val;
+ },
+ },
+ mounted() {
+ if (this.prop) {
+ const { addField } = this.FormContext;
+ addField && addField(this);
+ let initialValue = this.fieldValue;
+ if (Array.isArray(initialValue)) {
+ initialValue = [...initialValue];
+ }
+ this.initialValue = initialValue;
+ }
+ },
+ beforeDestroy() {
+ const { removeField } = this.FormContext;
+ removeField && removeField(this);
+ },
+ methods: {
+ validate(trigger, callback = noop) {
+ this.validateDisabled = false;
+ const rules = this.getFilteredRule(trigger);
+ if ((!rules || rules.length === 0) && this.required === undefined) {
+ callback();
+ return true;
+ }
+ this.validateState = 'validating';
+ const descriptor = {};
+ if (rules && rules.length > 0) {
+ rules.forEach(rule => {
+ delete rule.trigger;
+ });
+ }
+ descriptor[this.prop] = rules;
+ const validator = new AsyncValidator(descriptor);
+ const model = {};
+ model[this.prop] = this.fieldValue;
+ validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
+ this.validateState = errors ? 'error' : 'success';
+ this.validateMessage = errors ? errors[0].message : '';
+ callback(this.validateMessage, invalidFields);
+ this.FormContext &&
+ this.FormContext.$emit &&
+ this.FormContext.$emit('validate', this.prop, !errors, this.validateMessage || null);
+ });
+ },
+ getRules() {
+ let formRules = this.FormContext.rules || {};
+ const selfRules = this.rules;
+ const requiredRule = this.required !== undefined ? { required: !!this.required } : [];
+ const prop = getPropByPath(formRules, this.prop || '');
+ formRules = formRules ? prop.o[this.prop || ''] || prop.v : [];
+ return [...selfRules, ...formRules, ...requiredRule];
+ },
+ getFilteredRule(trigger) {
+ const rules = this.getRules();
+ return rules
+ .filter(rule => {
+ if (!rule.trigger || trigger === '') return true;
+ if (Array.isArray(rule.trigger)) {
+ return rule.trigger.indexOf(trigger) > -1;
+ } else {
+ return rule.trigger === trigger;
+ }
+ })
+ .map(rule => ({ ...rule }));
+ },
+ onFieldBlur() {
+ this.validate('blur');
+ },
+ onFieldChange() {
+ if (this.validateDisabled) {
+ this.validateDisabled = false;
+ return;
+ }
+ this.validate('change');
+ },
+ clearValidate() {
+ this.validateState = '';
+ this.validateMessage = '';
+ this.validateDisabled = false;
+ },
+ resetField() {
+ this.validateState = '';
+ this.validateMessage = '';
+ let model = this.FormContext.model || {};
+ let value = this.fieldValue;
+ let path = this.prop;
+ if (path.indexOf(':') !== -1) {
+ path = path.replace(/:/, '.');
+ }
+ let prop = getPropByPath(model, path, true);
+ this.validateDisabled = true;
+ if (Array.isArray(value)) {
+ prop.o[prop.k] = [].concat(this.initialValue);
+ } else {
+ prop.o[prop.k] = this.initialValue;
+ }
+ // reset validateDisabled after onFieldChange triggered
+ this.$nextTick(() => {
+ this.validateDisabled = false;
+ });
+ },
+ },
+ render() {
+ const { $slots } = this;
+ const props = getOptionProps(this);
+ const label = getComponentFromProp(this, 'label');
+ const extra = getComponentFromProp(this, 'extra');
+ const help = getComponentFromProp(this, 'help');
+ const formProps = {
+ props: {
+ ...props,
+ label,
+ extra,
+ validateStatus: this.validateState,
+ help: this.validateMessage || help,
+ },
+ };
+ return {$slots.default};
+ },
+};
diff --git a/components/n-form/__tests__/demo.test.js b/components/n-form/__tests__/demo.test.js
new file mode 100644
index 000000000..8f8e33928
--- /dev/null
+++ b/components/n-form/__tests__/demo.test.js
@@ -0,0 +1,3 @@
+import demoTest from '../../../tests/shared/demoTest';
+
+demoTest('n-form');
diff --git a/components/n-form/index.jsx b/components/n-form/index.jsx
new file mode 100644
index 000000000..afc701b07
--- /dev/null
+++ b/components/n-form/index.jsx
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import Form from './Form';
+import ref from 'vue-ref';
+import FormDecoratorDirective from '../_util/FormDecoratorDirective';
+import Base from '../base';
+
+Vue.use(ref, { name: 'ant-ref' });
+Vue.use(FormDecoratorDirective);
+
+export { FormProps, ValidationRule } from './Form';
+export { FormItemProps } from './FormItem';
+
+/* istanbul ignore next */
+Form.install = function(Vue) {
+ Vue.use(Base);
+ Vue.component(Form.name, Form);
+ Vue.component(Form.Item.name, Form.Item);
+};
+
+export default Form;
diff --git a/components/n-form/style/index.js b/components/n-form/style/index.js
new file mode 100644
index 000000000..30c84ff81
--- /dev/null
+++ b/components/n-form/style/index.js
@@ -0,0 +1,5 @@
+import '../../style/index.less';
+import './index.less';
+
+// style dependencies
+import '../../grid/style';
diff --git a/components/n-form/style/index.less b/components/n-form/style/index.less
new file mode 100644
index 000000000..1967010e3
--- /dev/null
+++ b/components/n-form/style/index.less
@@ -0,0 +1,653 @@
+@import '../../style/themes/index';
+@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: @font-size-base;
+@form-help-margin-top: (@form-component-height - @form-component-max-height) / 2 + 2px;
+@form-explain-font-size: @font-size-base;
+// Extends additional 1px to fix precision issue.
+// https://github.com/ant-design/ant-design/issues/12803
+// https://github.com/ant-design/ant-design/issues/8220
+@form-explain-precision: 1px;
+@form-explain-height: floor(@form-explain-font-size * @line-height-base);
+
+.@{form-prefix-cls} {
+ .reset-component;
+ .reset-form;
+}
+
+.@{form-prefix-cls}-item-required::before {
+ display: inline-block;
+ margin-right: 4px;
+ color: @label-required-color;
+ font-size: @font-size-base;
+ font-family: SimSun, sans-serif;
+ line-height: 1;
+ content: '*';
+ .@{form-prefix-cls}-hide-required-mark & {
+ display: none;
+ }
+}
+
+.@{form-prefix-cls}-item-label > label {
+ color: @label-color;
+
+ &::after {
+ & when (@form-item-trailing-colon=true) {
+ content: ':';
+ }
+ & when not (@form-item-trailing-colon=true) {
+ content: ' ';
+ }
+
+ position: relative;
+ top: -0.5px;
+ margin: 0 @form-item-label-colon-margin-right 0 @form-item-label-colon-margin-left;
+ }
+
+ &.@{form-prefix-cls}-item-no-colon::after {
+ content: ' ';
+ }
+}
+
+// Form items
+// You should wrap labels and controls in .@{form-prefix-cls}-item for optimum spacing
+.@{form-prefix-cls}-item {
+ label {
+ position: relative;
+
+ > .@{iconfont-css-prefix} {
+ font-size: @font-size-base;
+ vertical-align: top;
+ }
+ }
+
+ .reset-component;
+
+ margin-bottom: @form-item-margin-bottom;
+ vertical-align: top;
+
+ &-control {
+ position: relative;
+ line-height: @form-component-max-height;
+ .clearfix;
+ }
+
+ &-children {
+ position: relative;
+ }
+
+ &-with-help {
+ margin-bottom: max(0, @form-item-margin-bottom - @form-explain-height - @form-help-margin-top);
+ }
+
+ &-label {
+ display: inline-block;
+ overflow: hidden;
+ line-height: @form-component-max-height - 0.0001px;
+ white-space: nowrap;
+ text-align: right;
+ vertical-align: middle;
+
+ &-left {
+ text-align: left;
+ }
+ }
+
+ .@{ant-prefix}-switch {
+ margin: 2px 0 4px;
+ }
+}
+
+.@{form-prefix-cls}-explain,
+.@{form-prefix-cls}-extra {
+ clear: both;
+ min-height: @form-explain-height + @form-explain-precision;
+ margin-top: @form-help-margin-top;
+ color: @text-color-secondary;
+ font-size: @form-explain-font-size;
+ line-height: @line-height-base;
+ transition: color 0.3s @ease-out; // sync input color transition
+}
+
+.@{form-prefix-cls}-explain {
+ margin-bottom: -@form-explain-precision;
+}
+
+.@{form-prefix-cls}-extra {
+ padding-top: 4px;
+}
+
+.@{form-prefix-cls}-text {
+ display: inline-block;
+ padding-right: 8px;
+}
+
+.@{form-prefix-cls}-split {
+ display: block;
+ text-align: center;
+}
+
+form {
+ .has-feedback {
+ .@{ant-prefix}-input {
+ padding-right: @input-padding-horizontal-base + @input-affix-width;
+ }
+
+ // https://github.com/ant-design/ant-design/issues/19884
+ .@{ant-prefix}-input-affix-wrapper {
+ .@{ant-prefix}-input-suffix {
+ padding-right: 18px;
+ }
+ .@{ant-prefix}-input {
+ padding-right: @input-padding-horizontal-base + @input-affix-width * 2;
+ }
+ &.@{ant-prefix}-input-affix-wrapper-input-with-clear-btn {
+ .@{ant-prefix}-input {
+ padding-right: @input-padding-horizontal-base + @input-affix-width * 3;
+ }
+ }
+ }
+
+ // Fix overlapping between feedback icon and