diff --git a/examples/docs/zh-cn/form.md b/examples/docs/zh-cn/form.md index ab4589def..141fe6cc2 100644 --- a/examples/docs/zh-cn/form.md +++ b/examples/docs/zh-cn/form.md @@ -792,8 +792,8 @@ |---------- |-------------- |---------- |-------------------------------- |-------- | | model | 表单数据对象 | object | — | — | | rules | 表单验证规则 | object | — | — | -| type | 表单类型 | string | stacked, inline, horizontal | horizontal | -| label-align | 表单域标签的水平对齐位置 | string | right,left | right | +| inline | 行内表单模式 | boolean | — | false | +| label-position | 表单域标签的位置 | string | right/left/top | right | | label-width | 表单域标签的宽度,所有的 form-item 都会继承 form 组件的 labelWidth 的值 | string | — | — | | label-suffix | 表单域标签的后缀 | string | — | — | diff --git a/examples/docs/zh-cn/table.md b/examples/docs/zh-cn/table.md index 00ccd076d..6a4366d89 100644 --- a/examples/docs/zh-cn/table.md +++ b/examples/docs/zh-cn/table.md @@ -918,6 +918,7 @@ | cell-mouse-leave | 当单元格 hover 退出时会触发该事件 | row, column, cell, event | | cell-click | 当某个单元格被点击时会触发该事件 | row, column, cell, event | | row-click | 当某一行被点击时会触发该事件 | row, event | +| sort-change | 当表格的排序条件发生变化的时候会触发该事件 | { column, prop, order } | ### Table Methods | 方法名 | 说明 | 参数 | @@ -931,7 +932,8 @@ | prop | 对应列内容的字段名,也可以使用 property 属性 | string | — | — | | width | 对应列的宽度 | string | — | — | | fixed | 列是否固定在左侧或者右侧,true 表示固定在左侧 | string, boolean | true, left, right | - | -| sortable | 对应列是否可以排序 | boolean | — | false | +| sortable | 对应列是否可以排序,如果设置为 'custom',则代表用户希望远程排序,需要监听 Table 的 sort-change 事件 | boolean, string | true, false, 'custom' | false | +| sort-method | 对数据进行排序的时候使用的方法,仅当 sortable 设置为 true 的时候有效 | Function(a, b) | - | - | | resizable | 对应列是否可以通过拖动改变宽度(如果需要在 el-table 上设置 border 属性为真) | boolean | — | true | | type | 对应列的类型。如果设置了 `selection` 则显示多选框,如果设置了 `index` 则显示该行的索引(从 1 开始计算) | string | selection/index | — | | formatter | 用来格式化内容 | Function(row, column) | — | — | diff --git a/packages/form/src/form-item.vue b/packages/form/src/form-item.vue index 6dbf1b003..92266f9f2 100644 --- a/packages/form/src/form-item.vue +++ b/packages/form/src/form-item.vue @@ -85,7 +85,6 @@ methods: { validate(trigger, cb) { var rules = this.getFilteredRule(trigger); - if (!rules || rules.length === 0) { cb && cb(); return true; diff --git a/packages/form/src/form.vue b/packages/form/src/form.vue index adebb2e10..bc47ab9dd 100644 --- a/packages/form/src/form.vue +++ b/packages/form/src/form.vue @@ -15,7 +15,6 @@ props: { model: Object, rules: Object, - type: String, labelPosition: String, labelWidth: String, labelSuffix: { @@ -35,6 +34,7 @@ this.fields[field.prop] = field; this.fieldLength++; }); + /* istanbul ignore next */ this.$on('el.form.removeField', (field) => { delete this.fields[field.prop]; this.fieldLength--; diff --git a/packages/table/src/table-column.js b/packages/table/src/table-column.js index 4ce4f6f29..0e235de3a 100644 --- a/packages/table/src/table-column.js +++ b/packages/table/src/table-column.js @@ -6,19 +6,19 @@ let columnIdSeed = 1; const defaults = { default: { - direction: '' + order: '' }, selection: { width: 48, minWidth: 48, realWidth: 48, - direction: '' + order: '' }, index: { width: 48, minWidth: 48, realWidth: 48, - direction: '' + order: '' } }; @@ -98,9 +98,10 @@ export default { minWidth: {}, template: String, sortable: { - type: Boolean, + type: [Boolean, String], default: false }, + sortMethod: Function, resizable: { type: Boolean, default: true @@ -201,6 +202,7 @@ export default { isColumnGroup, align: this.align ? 'is-' + this.align : null, sortable: this.sortable, + sortMethod: this.sortMethod, resizable: this.resizable, showTooltipWhenOverflow: this.showTooltipWhenOverflow, formatter: this.formatter, diff --git a/packages/table/src/table-header.js b/packages/table/src/table-header.js index ee2d15c84..9753a0829 100644 --- a/packages/table/src/table-header.js +++ b/packages/table/src/table-header.js @@ -33,7 +33,7 @@ export default { on-mousemove={ ($event) => this.handleMouseMove($event, column) } on-mouseout={ this.handleMouseOut } on-mousedown={ ($event) => this.handleMouseDown($event, column) } - class={ [column.id, column.direction, column.align, this.isCellHidden(cellIndex) ? 'hidden' : ''] }> + class={ [column.id, column.order, column.align, this.isCellHidden(cellIndex) ? 'hidden' : ''] }>
0 ? 'highlight' : ''] }> { column.headerTemplate @@ -269,26 +269,30 @@ export default { if (!column.sortable) return; - const sortCondition = this.store.states.sortCondition; + const states = this.store.states; + let sortProp = states.sortProp; + let sortOrder; + const sortingColumn = states.sortingColumn; - if (sortCondition.column !== column) { - if (sortCondition.column) { - sortCondition.column.direction = ''; + if (sortingColumn !== column) { + if (sortingColumn) { + sortingColumn.order = null; } - sortCondition.column = column; - sortCondition.property = column.property; + states.sortingColumn = column; + sortProp = column.property; } - if (!column.direction) { - column.direction = 'ascending'; - } else if (column.direction === 'ascending') { - column.direction = 'descending'; + if (!column.order) { + sortOrder = column.order = 'ascending'; + } else if (column.order === 'ascending') { + sortOrder = column.order = 'descending'; } else { - column.direction = ''; - sortCondition.column = null; - sortCondition.property = null; + sortOrder = column.order = null; + states.sortingColumn = null; + sortProp = null; } - sortCondition.direction = column.direction === 'descending' ? -1 : 1; + states.sortProp = sortProp; + states.sortOrder = sortOrder; this.store.commit('changeSortCondition'); } diff --git a/packages/table/src/table-store.js b/packages/table/src/table-store.js index e844b0478..9f37d836b 100644 --- a/packages/table/src/table-store.js +++ b/packages/table/src/table-store.js @@ -11,6 +11,14 @@ const getRowIdentity = (row, rowKey) => { } }; +const sortData = (data, states) => { + const sortingColumn = states.sortingColumn; + if (!sortingColumn || typeof sortingColumn.sortable === 'string') { + return data; + } + return orderBy(data, states.sortProp, states.sortOrder, sortingColumn.sortMethod); +}; + const TableStore = function(table, initialState = {}) { if (!table) { throw new Error('Table is required.'); @@ -26,11 +34,9 @@ const TableStore = function(table, initialState = {}) { _data: null, filteredData: null, data: null, - sortCondition: { - column: null, - property: null, - direction: null - }, + sortingColumn: null, + sortProp: null, + sortOrder: null, isAllSelected: false, selection: [], reserveSelection: false, @@ -52,7 +58,7 @@ TableStore.prototype.mutations = { if (data && data[0] && typeof data[0].$selected === 'undefined') { data.forEach((item) => Vue.set(item, '$selected', false)); } - states.data = orderBy((data || []), states.sortCondition.property, states.sortCondition.direction); + states.data = sortData((data || []), states); if (!states.reserveSelection) { states.isAllSelected = false; @@ -82,7 +88,13 @@ TableStore.prototype.mutations = { }, changeSortCondition(states) { - states.data = orderBy((states.filteredData || states._data || []), states.sortCondition.property, states.sortCondition.direction); + states.data = sortData((states.filteredData || states._data || []), states); + + this.table.$emit('sort-change', { + column: this.states.sortingColumn, + prop: this.states.sortProp, + order: this.states.sortOrder + }); Vue.nextTick(() => this.table.updateScrollY()); }, @@ -113,7 +125,7 @@ TableStore.prototype.mutations = { }); states.filteredData = data; - states.data = orderBy(data, states.sortCondition.property, states.sortCondition.direction); + states.data = sortData(data, states); Vue.nextTick(() => this.table.updateScrollY()); }, diff --git a/packages/table/src/util.js b/packages/table/src/util.js index d7e3e1c2d..b05cd0228 100644 --- a/packages/table/src/util.js +++ b/packages/table/src/util.js @@ -58,14 +58,19 @@ const isObject = function(obj) { return obj !== null && typeof obj === 'object'; }; -export const orderBy = function(array, sortKey, reverse) { +export const orderBy = function(array, sortKey, reverse, sortMethod) { + if (typeof reverse === 'string') { + reverse = reverse === 'descending' ? -1 : 1; + } if (!sortKey) { return array; } const order = (reverse && reverse < 0) ? -1 : 1; // sort on a copy to avoid mutating original array - return array.slice().sort(function(a, b) { + return array.slice().sort(sortMethod ? function(a, b) { + return sortMethod(a, b) ? order : -order; + } : function(a, b) { if (sortKey !== '$key') { if (isObject(a) && '$value' in a) a = a.$value; if (isObject(b) && '$value' in b) b = b.$value; diff --git a/test/unit/specs/dropdown.spec.js b/test/unit/specs/dropdown.spec.js index 26a0452f5..1aedc8fea 100644 --- a/test/unit/specs/dropdown.spec.js +++ b/test/unit/specs/dropdown.spec.js @@ -4,7 +4,7 @@ describe('Dropdown', () => { it('create', done => { const vm = createVue({ template: ` - + 下拉菜单 @@ -18,19 +18,19 @@ describe('Dropdown', () => { ` }, true); - let dropdownElm = vm.$el; + let dropdown = vm.$refs.dropdown; + let dropdownElm = dropdown.$el; let triggerElm = dropdownElm.children[0]; triggerEvent(triggerElm, 'mouseenter'); setTimeout(_ => { - var dropdownMenu = document.querySelector('.dropdown-test-creat'); - expect(dropdownMenu.style.display).to.not.ok; + expect(dropdown.visible).to.be.true; triggerEvent(triggerElm, 'mouseleave'); setTimeout(_ => { - expect(dropdownMenu.style.display).to.be.equal('none'); + expect(dropdown.visible).to.not.true; done(); - }, 600); + }, 300); }, 400); }); it('menu click', done => { diff --git a/test/unit/specs/form.spec.js b/test/unit/specs/form.spec.js new file mode 100644 index 000000000..9d5639bdd --- /dev/null +++ b/test/unit/specs/form.spec.js @@ -0,0 +1,577 @@ +import { createVue } from '../util'; + +describe('Form', () => { + it('label width', done => { + const vm = createVue({ + template: ` + + + + + + `, + data() { + return { + form: { + name: '' + } + }; + } + }, true); + expect(vm.$el.querySelector('.el-form-item__label').style.width).to.equal('80px'); + expect(vm.$el.querySelector('.el-form-item__content').style.marginLeft).to.equal('80px'); + done(); + }); + it('inline form', done => { + const vm = createVue({ + template: ` + + + + + + + + + `, + data() { + return { + form: { + name: '', + address: '' + } + }; + } + }, true); + expect(vm.$el.classList.contains('el-form--inline')).to.be.true; + done(); + }); + it('label position', done => { + const vm = createVue({ + template: ` +
+ + + + + + + + + + + + + + + + +
+ `, + data() { + return { + form: { + name: '', + address: '' + } + }; + } + }, true); + expect(vm.$refs.labelTop.$el.classList.contains('el-form--label-top')).to.be.true; + expect(vm.$refs.labelLeft.$el.classList.contains('el-form--label-left')).to.be.true; + done(); + }); + it('reset field', done => { + const vm = createVue({ + template: ` + + + + + + + + + + + + + + + + + `, + data() { + return { + form: { + name: '', + address: '', + type: [] + }, + rules: { + name: [ + { required: true, message: '请输入活动名称', trigger: 'blur' } + ], + address: [ + { required: true, message: '请选择活动区域', trigger: 'change' } + ], + type: [ + { type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' } + ] + } + }; + }, + methods: { + setValue() { + this.form.name = 'jack'; + this.form.address = 'aaaa'; + this.form.type.push('地推活动'); + } + } + }, true); + vm.setValue(); + vm.$refs.form.resetFields(); + vm.$refs.form.$nextTick(_ => { + expect(vm.form.name).to.equal(''); + expect(vm.form.address).to.equal(''); + expect(vm.form.type.length).to.equal(0); + done(); + }); + }); + it('form item nest', done => { + const vm = createVue({ + template: ` + + + + + + + + - + + + + + + + + `, + data() { + return { + form: { + date1: '', + date2: '' + }, + rules: { + date1: [ + { type: 'date', required: true, message: '请选择日期', trigger: 'change' } + ] + } + }; + }, + methods: { + setValue() { + this.name = 'jack'; + this.address = 'aaaa'; + } + } + }, true); + vm.$refs.form.validate(valid => { + expect(valid).to.not.true; + done(); + }); + }); + describe('validate', () => { + it('input', done => { + const vm = createVue({ + template: ` + + + + + + `, + data() { + return { + form: { + name: '' + }, + rules: { + name: [ + { required: true, message: '请输入活动名称', trigger: 'change', min: 3, max: 6 } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.name = value; + } + } + }, true); + vm.$refs.form.validate(valid => { + let fields = vm.$refs.form.fields; + expect(valid).to.not.true; + vm.$refs.form.$nextTick(_ => { + expect(fields.name.error).to.equal('请输入活动名称'); + vm.setValue('aaaaa'); + + vm.$refs.form.$nextTick(_ => { + expect(fields.name.error).to.equal(''); + vm.setValue('aa'); + + vm.$refs.form.$nextTick(_ => { + expect(fields.name.error).to.equal('请输入活动名称'); + done(); + }); + }); + }); + }); + }); + it('textarea', done => { + const vm = createVue({ + template: ` + + + + + + `, + data() { + return { + form: { + name: '' + }, + rules: { + name: [ + { required: true, message: '请输入活动名称', trigger: 'change', min: 3, max: 6 } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.name = value; + } + } + }, true); + vm.$refs.form.validate(valid => { + let fields = vm.$refs.form.fields; + expect(valid).to.not.true; + vm.$refs.form.$nextTick(_ => { + expect(fields.name.error).to.equal('请输入活动名称'); + vm.setValue('aaaaa'); + + vm.$refs.form.$nextTick(_ => { + expect(fields.name.error).to.equal(''); + vm.setValue('aa'); + + vm.$refs.form.$nextTick(_ => { + expect(fields.name.error).to.equal('请输入活动名称'); + done(); + }); + }); + }); + }); + }); + it('selector', done => { + const vm = createVue({ + template: ` + + + + + + + + + `, + data() { + return { + form: { + region: 'shanghai' + }, + rules: { + region: [ + {required: true, message: '请选择活动区域', trigger: 'change' } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.region = value; + } + } + }, true); + vm.$refs.form.validate(valid => { + let fields = vm.$refs.form.fields; + expect(valid).to.true; + vm.setValue(''); + setTimeout(_ => { + expect(fields.region.error).to.equal('请选择活动区域'); + vm.setValue('shanghai'); + + setTimeout(_ => { + expect(fields.region.error).to.equal(''); + done(); + }, 100); + }, 100); + }); + }); + it('datepicker', done => { + const vm = createVue({ + template: ` + + + + + + `, + data() { + return { + form: { + date: '' + }, + rules: { + date: [ + {type: 'date', required: true, message: '请选择日期', trigger: 'change' } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.date = value; + } + } + }, true); + vm.$refs.form.validate(valid => { + let fields = vm.$refs.form.fields; + expect(valid).to.not.true; + vm.$refs.form.$nextTick(_ => { + expect(fields.date.error).to.equal('请选择日期'); + + vm.setValue(new Date()); + + vm.$refs.form.$nextTick(_ => { + expect(fields.date.error).to.equal(''); + done(); + }); + }); + }); + }); + it('timepicker', done => { + const vm = createVue({ + template: ` + + + + + + `, + data() { + return { + form: { + date: '' + }, + rules: { + date: [ + {type: 'date', required: true, message: '请选择时间', trigger: 'change' } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.date = value; + } + } + }, true); + vm.$refs.form.validate(valid => { + let fields = vm.$refs.form.fields; + expect(valid).to.not.true; + vm.$refs.form.$nextTick(_ => { + expect(fields.date.error).to.equal('请选择时间'); + vm.setValue(new Date()); + + vm.$refs.form.$nextTick(_ => { + expect(fields.date.error).to.equal(''); + done(); + }); + }); + }); + }); + it('checkbox group', done => { + const vm = createVue({ + template: ` + + + + + + + + + + + `, + data() { + return { + form: { + type: [] + }, + rules: { + type: [ + { type: 'array', required: true, message: '请选择活动类型', trigger: 'change' } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.type = value; + } + } + }, true); + vm.$refs.form.validate(valid => { + let fields = vm.$refs.form.fields; + expect(valid).to.not.true; + vm.$refs.form.$nextTick(_ => { + expect(fields.type.error).to.equal('请选择活动类型'); + vm.setValue(['地推活动']); + + vm.$refs.form.$nextTick(_ => { + expect(fields.type.error).to.equal(''); + done(); + }); + }); + }); + }); + it('radio group', done => { + const vm = createVue({ + template: ` + + + + + + + + + `, + data() { + return { + form: { + type: '' + }, + rules: { + type: [ + { required: true, message: '请选择活动类型', trigger: 'change' } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.type = value; + } + } + }, true); + vm.$refs.form.validate(valid => { + let fields = vm.$refs.form.fields; + expect(valid).to.not.true; + vm.$refs.form.$nextTick(_ => { + expect(fields.type.error).to.equal('请选择活动类型'); + vm.setValue('线下场地免费'); + + vm.$refs.form.$nextTick(_ => { + expect(fields.type.error).to.equal(''); + done(); + }); + }); + }); + }); + it('validate field', done => { + const vm = createVue({ + template: ` + + + + + + `, + data() { + return { + form: { + name: '' + }, + rules: { + name: [ + { required: true, message: '请输入活动名称', trigger: 'change', min: 3, max: 6 } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.name = value; + } + } + }, true); + vm.$refs.form.validateField('name', valid => { + expect(valid).to.not.true; + done(); + }); + }); + it('custom validate', done => { + var checkName = (rule, value, callback) => { + if (value.length < 5) { + callback(new Error('长度至少为5')); + } else { + callback(); + } + }; + const vm = createVue({ + template: ` + + + + + + `, + data() { + return { + form: { + name: '' + }, + rules: { + name: [ + { validator: checkName, trigger: 'change' } + ] + } + }; + }, + methods: { + setValue(value) { + this.form.name = value; + } + } + }, true); + vm.$refs.form.validate(valid => { + let fields = vm.$refs.form.fields; + expect(valid).to.not.true; + vm.$refs.form.$nextTick(_ => { + expect(fields.name.error).to.equal('长度至少为5'); + vm.setValue('aaaaaa'); + + vm.$refs.form.$nextTick(_ => { + expect(fields.name.error).to.equal(''); + done(); + }); + }); + }); + }); + }); +});