Browse Source

dynamic tabs (#812)

baiyaaaaa 8 years ago
parent
commit
8eae476f7f

+ 1 - 0
CHANGELOG.md

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

+ 14 - 6
examples/docs/zh-cn/tabs.md

@@ -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: ''}
+        ]
       };
     }
   };

+ 7 - 8
packages/tabs/src/tab-pane.vue

@@ -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>

+ 0 - 30
packages/tabs/src/tab.vue

@@ -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>

+ 74 - 62
packages/tabs/src/tabs.vue

@@ -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();
-        tab.$destroy(true);
-
-        var index = this.tabs.indexOf(tab);
+      handleTabRemove(tab, event) {
+        event.stopPropagation();
+        let tabs = this.$children;
 
-        if (index !== -1) {
-          this.tabs.splice(index, 1);
-        }
+        var index = tabs.indexOf(tab);
+        tab.$destroy(true);
 
-        if (tab.key === this.currentName) {
-          let nextChild = this.tabs[index];
-          let prevChild = this.tabs[index - 1];
+        if (tab.index === this.currentName) {
+          let nextChild = tabs[index];
+          let prevChild = 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>

+ 3 - 3
packages/theme-default/src/tabs.css

@@ -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;
     }

+ 15 - 16
test/unit/specs/tabs.spec.js

@@ -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>