dynamic tabs (#812)

pull/816/head
baiyaaaaa 2016-11-04 11:28:23 +08:00 committed by cinwell.li
parent cb5572f9d7
commit 8eae476f7f
7 changed files with 114 additions and 125 deletions

View File

@ -12,6 +12,7 @@
- 新增 Dropdown 的 command api #432
- 修复 Slider 在 Form 中的显示问题
- 修复 Upload 在 onSuccess、onError 钩子无法拿到服务端返回信息的问题
- 改善 tabs 现在支持动态更新
#### 非兼容性更新

View File

@ -3,7 +3,13 @@
data() {
return {
activeName: 'first',
activeName2: ''
activeName2: '',
tabs: [
{label: '用户管理', content: ''},
{label: '配置管理', content: ''},
{label: '角色管理', content: ''},
{label: '定时任务补偿', content: ''}
]
}
},
methods: {
@ -28,17 +34,19 @@
```html
<template>
<el-tabs>
<el-tab-pane label="用户管理"></el-tab-pane>
<el-tab-pane label="配置管理"></el-tab-pane>
<el-tab-pane label="角色管理"></el-tab-pane>
<el-tab-pane label="定时任务补偿"></el-tab-pane>
<el-tab-pane v-for="tab in tabs" :label="tab.label">{{tab.content}}</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
data() {
return {
activeName: 'first'
tabs: [
{label: '用户管理', content: ''},
{label: '配置管理', content: ''},
{label: '角色管理', content: ''},
{label: '定时任务补偿', content: ''}
]
};
}
};

View File

@ -17,19 +17,19 @@
paneStyle: {
position: 'relative'
},
key: ''
index: ''
};
},
created() {
if (!this.key) {
this.key = this.$parent.$children.indexOf(this) + 1 + '';
if (!this.index) {
this.index = this.$parent.$children.indexOf(this) + 1 + '';
}
},
computed: {
show() {
return this.$parent.currentName === this.key;
return this.$parent.currentName === this.index;
}
},
@ -41,21 +41,20 @@
name: {
immediate: true,
handler(val) {
this.key = val;
this.index = val;
}
},
'$parent.currentName'(newValue, oldValue) {
if (this.key === newValue) {
if (this.index === newValue) {
this.transition = newValue > oldValue ? 'slideInRight' : 'slideInLeft';
}
if (this.key === oldValue) {
if (this.index === oldValue) {
this.transition = oldValue > newValue ? 'slideInRight' : 'slideInLeft';
}
}
}
};
</script>
<template>
<div class="el-tab-pane" v-show="show && $slots.default">
<slot></slot>

View File

@ -1,30 +0,0 @@
<script>
module.exports = {
name: 'el-tab',
props: {
tab: {
type: Object,
required: true
},
closable: Boolean
}
};
</script>
<template>
<div
class="el-tabs__item"
:class="{
'is-active': $parent.currentName === tab.key,
'is-disabled': tab.disabled,
'is-closable': closable
}">
{{tab.label}}
<span
class="el-icon-close"
v-if="closable"
@click="$emit('remove', tab, $event)">
</span>
</div>
</template>

View File

@ -1,13 +1,7 @@
<script>
import ElTab from './tab';
module.exports = {
name: 'el-tabs',
components: {
ElTab
},
props: {
type: String,
tabPosition: String,
@ -18,11 +12,9 @@
data() {
return {
tabs: [],
children: null,
activeTab: null,
currentName: 0,
barStyle: ''
currentName: 0
};
},
@ -31,45 +23,40 @@
handler(val) {
this.currentName = val;
}
},
'currentName'() {
this.calcBarStyle();
}
},
methods: {
handleTabRemove(tab, ev) {
ev.stopPropagation();
handleTabRemove(tab, event) {
event.stopPropagation();
let tabs = this.$children;
var index = tabs.indexOf(tab);
tab.$destroy(true);
var index = this.tabs.indexOf(tab);
if (tab.index === this.currentName) {
let nextChild = tabs[index];
let prevChild = tabs[index - 1];
if (index !== -1) {
this.tabs.splice(index, 1);
}
if (tab.key === this.currentName) {
let nextChild = this.tabs[index];
let prevChild = this.tabs[index - 1];
this.currentName = nextChild ? nextChild.key : prevChild ? prevChild.key : '-1';
this.currentName = nextChild ? nextChild.index : prevChild ? prevChild.index : '-1';
}
this.$emit('tab-remove', tab);
this.$forceUpdate();
},
handleTabClick(tab, event) {
this.currentName = tab.key;
this.currentName = tab.index;
this.$emit('tab-click', tab, event);
},
calcBarStyle(firstRendering) {
calcBarStyle() {
if (this.type || !this.$refs.tabs) return {};
var style = {};
var offset = 0;
var tabWidth = 0;
this.tabs.every((tab, index) => {
let $el = this.$refs.tabs[index].$el;
if (tab.key !== this.currentName) {
this.$children.every((panel, index) => {
let $el = this.$refs.tabs[index];
if (!$el) { return false; }
if (panel.index !== this.currentName) {
offset += $el.clientWidth;
return true;
} else {
@ -81,41 +68,66 @@
style.width = tabWidth + 'px';
style.transform = `translateX(${offset}px)`;
if (!firstRendering) {
style.transition = 'transform .3s cubic-bezier(.645,.045,.355,1), -webkit-transform .3s cubic-bezier(.645,.045,.355,1)';
}
this.barStyle = style;
return style;
}
},
mounted() {
var fisrtKey = this.$children[0].key || '1';
this.currentName = this.activeName || fisrtKey;
this.$children.forEach(tab => this.tabs.push(tab));
this.$nextTick(() => this.calcBarStyle(true));
this.$nextTick(() => {
this.currentName = this.activeName || this.$children[0].index || '1';
});
},
render(h) {
let {
type,
closable,
handleTabRemove,
handleTabClick,
currentName
} = this;
const barStyle = this.calcBarStyle();
const activeBar = !type
? <div class="el-tabs__active-bar" style={barStyle}></div>
: null;
const tabs = this.$children.map((tab, index) => {
let btnClose = h('span', {
class: {
'el-icon-close': true
},
on: { click: (ev) => { handleTabRemove(tab, ev); } }
});
const _tab = h('div', {
class: {
'el-tabs__item': true,
'is-active': currentName === tab.index,
'is-disabled': tab.disabled,
'is-closable': closable
},
ref: 'tabs',
refInFor: true,
on: { click: (ev) => { handleTabClick(tab, ev); } }
}, [
tab.label,
closable ? btnClose : null,
index === 0 ? activeBar : null
]);
return _tab;
});
return (
<div class={{
'el-tabs': true,
'el-tabs--card': type === 'card',
'el-tabs--border-card': type === 'border-card'
}}>
<div class="el-tabs__header">
{tabs}
</div>
<div class="el-tabs__content">
{this.$slots.default}
</div>
</div>
);
}
};
</script>
<template>
<div class="el-tabs" :class="[type ? 'el-tabs--' + type : '']">
<div class="el-tabs__header">
<el-tab
v-for="tab in tabs"
ref="tabs"
:tab="tab"
:closable="closable"
@remove="handleTabRemove"
@click.native="handleTabClick(tab, $event)">
</el-tab>
<div
class="el-tabs__active-bar"
:style="barStyle"
v-if="!this.type && tabs.length > 0">
</div>
</div>
<div class="el-tabs__content">
<slot></slot>
</div>
</div>
</template>

View File

@ -14,12 +14,13 @@
}
@e active-bar {
position: absolute;
bottom: -1px;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: var(--color-primary);
z-index: 1;
/*transition: transform .3s cubic-bezier(.645,.045,.355,1);*/
transition: transform .3s cubic-bezier(.645,.045,.355,1);
list-style: none;
}
@e item {
@ -44,7 +45,6 @@
}
}
@e content {
white-space: nowrap;
overflow: hidden;
position: relative;
}

View File

@ -9,22 +9,27 @@ describe('Tabs', () => {
it('create', done => {
vm = createVue({
template: `
<el-tabs>
<el-tabs ref="tabs">
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="角色管理">C</el-tab-pane>
<el-tab-pane label="角色管理" ref="pane-click">C</el-tab-pane>
<el-tab-pane label="定时任务补偿">D</el-tab-pane>
</el-tabs>
`
}, true);
let tabList = vm.$el.querySelector('.el-tabs__header').children;
let paneList = vm.$el.querySelector('.el-tabs__content').children;
let spy = sinon.spy();
vm.$refs.tabs.$on('tab-click', spy);
setTimeout(_ => {
expect(tabList[0].classList.contains('is-active')).to.be.true;
expect(paneList[0].style.display).to.not.ok;
tabList[2].click();
vm.$nextTick(_ => {
expect(spy.withArgs(vm.$refs['pane-click']).calledOnce).to.true;
expect(tabList[2].classList.contains('is-active')).to.be.true;
expect(paneList[2].style.display).to.not.ok;
done();
@ -98,33 +103,27 @@ describe('Tabs', () => {
it('closable', done => {
vm = createVue({
template: `
<el-tabs type="card" closable @tab-remove="handleRemove">
<el-tabs type="card" :closable="true" ref="tabs">
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="角色管理">C</el-tab-pane>
<el-tab-pane label="定时任务补偿">D</el-tab-pane>
</el-tabs>
`,
data() {
return {
removeTabName: ''
};
},
methods: {
handleRemove(tab) {
this.removeTabName = tab.label;
}
}
`
}, true);
let tabList = vm.$el.querySelector('.el-tabs__header').children;
let paneList = vm.$el.querySelector('.el-tabs__content').children;
let spy = sinon.spy();
vm.$refs.tabs.$on('tab-remove', spy);
setTimeout(_ => {
tabList[1].querySelector('.el-icon-close').click();
vm.$nextTick(_ => {
expect(tabList.length).to.be.equal(3);
expect(paneList.length).to.be.equal(3);
expect(vm.removeTabName).to.be.equal('配置管理');
expect(spy.calledOnce).to.true;
expect(tabList[1].innerText.trim()).to.be.equal('角色管理');
expect(paneList[0].innerText.trim()).to.be.equal('A');
done();
@ -134,7 +133,7 @@ describe('Tabs', () => {
it('closable edge', done => {
vm = createVue({
template: `
<el-tabs type="card" closable>
<el-tabs type="card" :closable="true">
<el-tab-pane label="用户管理">A</el-tab-pane>
<el-tab-pane label="配置管理">B</el-tab-pane>
<el-tab-pane label="角色管理">C</el-tab-pane>