首页 > 编程 > JavaScript > 正文

Vue render函数实战之实现tabs选项卡组件

2019-11-19 11:44:05
字体:
来源:转载
供稿:网友

用过Element ui库的童鞋肯定知道<el-tabs>组件,简单、好用、可以自定义标签页,不知道广大童鞋们在刚开始使用<el-tabs>组件的时候有没有想过它是如何实现的?我咋刚开始使用<el-tabs>组件的时候就有去想过,也想去实现一个超级简单的tabs选项卡组件,无奈当时功力不够,未能实现。最近的一个简单项目中正好要用到选项卡组件,由于项目简单也就没有使用任何第三方库,于是就自己动手写了个选项卡组件。

1、实现tabs选项卡组件的思考

<el-tabs v-model="activeName" @tab-click="handleClick"> <el-tab-pane label="用户管理" name="first">用户管理</el-tab-pane> <el-tab-pane label="配置管理" name="second">配置管理</el-tab-pane> <el-tab-pane label="角色管理" name="third">角色管理</el-tab-pane> <el-tab-pane label="定时任务补偿" name="fourth">定时任务补偿</el-tab-pane></el-tabs>

问题:

  1. 如何根据<el-tab-pane>来生成标签页?
  2. 如何过滤<el-tabs>组件中的子元素,使得在使用的时候只显示<el-tab-pane>,而不会显示其他组件或div之类的元素?

2、实现思路

想根据<el-tab-pane>来生成标签页就需要使用到<slot>,使用<slot>用<template>的形式肯定是不行的,因为无法获取到<slot>的数量;使用<template>的形式行不通,那就只有使用render函数了
过滤<el-tabs>组件中的子元素也需要使用render函数

3、代码实现

index.js

import PTabs from './PTabs';import PTabPane from './PTabPane';export default function tabsInstall(Vue) { if(tabsInstall.installed){ return; } Vue.component('PTabs', PTabs); Vue.component('PTabPane', PTabPane);}

PTabs.vue

<script> import PTabNav from './PTabNav'; export default { name: "PTabs", props: {  value: {  type: [String, Number],  default: ''  },  beforeClick: {  type: Function,  default(){   return function () {};  }  } }, components: {  PTabNav }, data(){  return {  pTabPanes: [],  currentName: this.value || 0  } }, methods: {  addPane(pane){  this.pTabPanes.push(pane);  if(!this.currentName){   this.setCurrentName(this.pTabPanes[0].name);  }  },  removePane(pane){  let index = this.pTabPanes.indexOf(pane);  if(index > -1){   this.pTabPanes.splice(index, 1);  }  },  setCurrentName(name){  if(this.currentName !== name){   this.currentName = name;   this.$emit('input', name);  }  },  // 标签页点击事件  handTabNavClick(name, pane, e){  if(this.currentName === name || pane.disabled){   return;  }  let before = this.beforeClick();  if(before && before.then){   before.then(() => {   this.setCurrentName(name);   this.$emit('tabClick', pane, e);   })  }else{   this.setCurrentName(name);   this.$emit('tabClick', pane, e);  }  } }, watch: {  value(newVal){  this.setCurrentName(newVal);  },  currentName(){  this.$nextTick(() => {   this.$refs.p_tab_nav.scrollToActiveTab();  });  } }, render(h) {  let {$scopedSlots} = this;  let $default = $scopedSlots.default();  let qTabPanes = $default.map(item => {  /* 过滤<PTabs>xxx</PTabs>中传递的xxx内容。这里只接收<PTabPane>组件,因为我们需要根据<PTabPane>组件的数量来生成   * <PTabNav>组件,如果参差了其它节点则会导致不能正确生成<PTabNav>组件 */  if(item.componentOptions && item.componentOptions.tag === 'PTabPane'){   return item;  }  });  let qTab = h('PTabNav', {  props: {   // 将tab-pane传递给 <PTabNav>组件,<PTabNav>组件就知道要有多少个tab-item了   tabPanes: this.pTabPanes,   handTabNavClick: this.handTabNavClick  },  ref: 'p_tab_nav'  });  let qTabBody = h('div', {  staticClass: 'p-tabs_content'  }, qTabPanes);  console.log($default)  return h('div', {  staticClass: 'p-tabs'  }, [qTab, qTabBody]); }, mounted() {  //console.log(this)  this.$nextTick(() => {  this.$refs.p_tab_nav.scrollToActiveTab();  }); } }</script><style lang="stylus">.p-tabs{ .p-tabs_header{ position: relative; margin-bottom: 15px; &.is-scrollable{  padding-left: 20px;  padding-right: 20px; } } .p-tabs_nav-prev, .p-tabs_nav-next{ position: absolute; top: 0; width: 20px; height: 100%; display: none; &::before{  position: absolute;  content: ' ';  font-size: 0;  line-height: 0;  width: 10px;  height: 10px;  top: 50%;  left: 50%;  border-top: 1px solid #eee;  border-left: 1px solid #eee;  margin: -5px 0 0 -5px; } cursor: pointer; &.disabled{  cursor: default;  border-color: #aaa; } } .p-tabs_nav-prev{ left: 0; &:before{  transform: rotate(-45deg); } } .p-tabs_nav-next{ right: 0; &:before{  transform: rotate(135deg); } } .p-tabs_header{ &.is-scrollable{  .p-tabs_nav-prev,  .p-tabs_nav-next{  display: block;  } } } .p-tabs_nav-scroll{ overflow: hidden; } .p-tabs_nav-list{ position: relative; float: left; white-space: nowrap; transition: transform .3s; } .p-tabs_nav-item{ display: inline-block; height: 40px; line-height: 40px; padding: 0 20px; color: #fff; cursor: pointer; &.active, &:hover{  color: #ffb845; } &.disabled{  cursor: not-allowed;  color: #aaa;  &:hover{  color: #aaa;  } } } .p-tabs_content{ position: relative; overflow: hidden; } .p-tabs-pane{ color: #fff; }}</style>

PTabPane.vue

<template> <div class="p-tabs-pane" v-show="show"> <slot></slot> </div></template><script> export default { name: "PTabPane", props: {  label: {  type: String,  default: ''  },  name: {  type: [String, Number],  default: ''  },  disabled: {  type: Boolean,  default: false  } }, data(){  return {  loaded: false  } }, computed: {  show(){  if(this.$parent.currentName === this.name){   if(!this.loaded){   this.loaded = true;   }   return true;  }  return false;  } }, watch: {  label(){  // label更新的时候强制更新父组件,以触发PTabNav才能更新  this.$parent.$forceUpdate();  } }, mounted() {  // 当当前组件创建的时候将当前组件添加到父组件的pTabPanes中,以触发PTabNav才能更新  this.$parent.addPane(this); }, destroyed() {  if(this.$el && this.$el.parentNode){  this.$el.parentNode.removeChild(this.$el);  }  // 当当前组件销毁时需从父组件中的pTabPanes中移除当前组件,以触发PTabNav才能更新  this.$parent.removePane(this); } }</script>

PTabNav.vue

<script> function noop() {}; export default { name: "PTabNav", props: {  tabPanes: {  type: Array,  default(){   return [];  }  },  handTabNavClick: {  type: Function,  default(){   return function () {};  }  } }, data(){  return {  navPrevDisabled: true,  navNextDisabled: true,  // 控制左右箭头显示  scrollable: false,  listOffset: 0  } }, methods: {  navPrevClickEvent(){  if(!this.navPrevDisabled){   let navScrollW = this.$refs.nav_scroll.offsetWidth;   let navListW = this.$refs.nav_list.offsetWidth;   let maxTransformX = 0;   let transformX = this.listOffset - navScrollW;   if(transformX < maxTransformX){   transformX = maxTransformX;   }   if(transformX === this.listOffset){   return;   }   console.log('上一页按钮点击了', transformX);   this.listOffset = transformX;   if(transformX === 0){   this.navPrevDisabled = true;   this.navNextDisabled = false;   }else if(transformX === (navListW - navScrollW)){   this.navPrevDisabled = false;   this.navNextDisabled = true;   }else{   this.navPrevDisabled = false;   this.navNextDisabled = false;   }  }  },  navNextClickEvent(){  if(!this.navNextDisabled){   let navScrollW = this.$refs.nav_scroll.offsetWidth;   let navListW = this.$refs.nav_list.offsetWidth;   let maxTransformX = navListW - navScrollW;   let transformX = this.listOffset + navScrollW;   if(transformX > maxTransformX){   transformX = maxTransformX;   }   if(transformX === this.listOffset){   return;   }   console.log('下一页按钮点击了', transformX);   this.listOffset = transformX;   if(transformX === 0){   this.navPrevDisabled = true;   this.navNextDisabled = false;   }else if(transformX === (navListW - navScrollW)){   this.navPrevDisabled = false;   this.navNextDisabled = true;   }else{   this.navPrevDisabled = false;   this.navNextDisabled = false;   }  }  },  // 计算 .p-tabs_nav-list 是否溢出  calculateListSpilled(){  let navScrollW = this.$refs.nav_scroll.offsetWidth;  let navListW = this.$refs.nav_list.offsetWidth;  if(navScrollW < navListW){   this.scrollable = true;  }else{   if(this.listOffset > 0){   this.listOffset = 0;   }   this.scrollable = false;  }  },  // 滚动条滚动到激活的tab  scrollToActiveTab(){  if(this.scrollable){   this.$nextTick(() => {   let navScrollW = this.$refs.nav_scroll.offsetWidth;   let navList = this.$refs.nav_list;   let activeTab = navList.querySelector('.active');   let activeTabOffsetLeft = 0;   if(activeTab){    activeTabOffsetLeft = activeTab.offsetLeft;   }   let transformX = activeTabOffsetLeft + activeTab.offsetWidth - navScrollW;   transformX = transformX < 0 ? 0 : transformX;   this.listOffset = transformX;   if(transformX === 0){    this.navPrevDisabled = true;    this.navNextDisabled = false;   }else if(transformX === (navList.offsetWidth - navScrollW)){    this.navPrevDisabled = false;    this.navNextDisabled = true;   }else{    this.navPrevDisabled = false;    this.navNextDisabled = false;   }   });  }  } }, computed: {  listOffsetTran(){  console.log('dddd',`translateX(-${this.listOffset}px);`)  return {   transform: `translateX(-${this.listOffset}px)`  }  } }, render(h) { /*dom结构 <div class="p-tabs_header is-scrollable">  <span class="p-tabs_nav-prev disabled"></span>  <span class="p-tabs_nav-next"></span>  <div class="p-tabs_nav-scroll">  <div class="p-tabs_nav-list">   <div class="p-tabs_nav-item active">全部</div>   <div class="p-tabs_nav-item disabled">技术教学</div>   <div class="p-tabs_nav-item">新手教学</div>  </div>  </div> </div> */  let navPrev = h('span', {  staticClass: 'p-tabs_nav-prev',  'class': {   disabled: this.navPrevDisabled  },  on: {   click: this.navPrevClickEvent  }  });  let navNext = h('span', {  staticClass: 'p-tabs_nav-next',  'class': {   disabled: this.navNextDisabled  },  on: {   click: this.navNextClickEvent  }  });  // 生成标签页  let navItems = this.tabPanes.map(item => {  let $labelSlot = item.$scopedSlots.label ? item.$scopedSlots.label() : null;  let labelContent = $labelSlot ? $labelSlot : item.label;  return h('div', {   staticClass: 'p-tabs_nav-item',   'class': {   active: this.$parent.currentName === item.name,   disabled: item.disabled,   },   on: {   click: (e) => {    this.handTabNavClick(item.name, item, e);   }   }  }, [labelContent]);  });  let navScroll = h('div', {  staticClass: 'p-tabs_nav-scroll',  ref: 'nav_scroll'  }, [  h('div', {   staticClass: 'p-tabs_nav-list',   ref: 'nav_list',   style: this.listOffsetTran  }, [navItems])  ]);  return h('div', {  staticClass: 'p-tabs_header',  'class': {   'is-scrollable': this.scrollable  },  }, [navPrev, navNext, navScroll]); }, updated(){  this.calculateListSpilled(); }, mounted() {  this.calculateListSpilled(); } }</script>

4、使用

main.js

// 引入tabs组件import tabs from './components/p-tabs';// 全局注册p-tabs组件Vue.use(tabs);

页面中使用

<PTabs v-model="activeName"> <PTabPane label="用户管理" name="first">用户管理</PTabPane> <PTabPane label="配置管理" name="second">配置管理</PTabPane> <PTabPane label="角色管理" name="third">角色管理</PTabPane> <PTabPane label="定时任务补偿" name="fourth">定时任务补偿</PTabPane></PTabs>

总结

以上所述是小编给大家介绍的Vue render函数实战之实现tabs选项卡组件,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对武林网网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表