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


Posted in Javascript onApril 22, 2019

用过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、代码实现

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

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选项卡组件,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
javascript开发技术大全-第1章javascript概述
Jul 03 Javascript
js 判断脚本加载完毕的代码
Jul 13 Javascript
JavaScript判断IE版本型号
Jul 27 Javascript
asp.net+jquery.form实现图片异步上传的方法(附jquery.form.js下载)
May 05 Javascript
javascript cookie用法基础教程(概念,设置,读取及删除)
Sep 20 Javascript
使用vue框架 Ajax获取数据列表并用BootStrap显示出来
Apr 24 Javascript
jQuery分组选择器简单用法示例
Apr 04 jQuery
浅谈一个webpack构建速度优化误区
Jun 24 Javascript
layui实现checkbox的目录树tree的例子
Sep 12 Javascript
vue组件系列之TagsInput详解
May 14 Javascript
通过滑动翻页效果实现和移动端click事件问题
Jan 26 Javascript
用JS创建一个录屏功能
Nov 11 Javascript
详解Vue依赖收集引发的问题
Apr 22 #Javascript
JS大坑之19位数的Number型精度丢失问题详解
Apr 22 #Javascript
Vue $mount实战之实现消息弹窗组件
Apr 22 #Javascript
深入理解vue中的slot与slot-scope
Apr 22 #Javascript
浅析vue插槽和作用域插槽的理解
Apr 22 #Javascript
详解50行代码,Node爬虫练手项目
Apr 22 #Javascript
Vue匿名插槽与作用域插槽的合并和覆盖行为
Apr 22 #Javascript
You might like
PHP array操作10个小技巧分享
2011/06/23 PHP
深入apache host的配置详解
2013/06/09 PHP
php对关联数组循环遍历的实现方法
2015/03/13 PHP
typecho插件编写教程(五):核心代码
2015/05/28 PHP
php开发最强大的IDE编辑的phpstorm 2020.2配置Xdebug调试的详细教程
2020/08/17 PHP
JS实现div内部的文字或图片自动循环滚动代码
2013/04/19 Javascript
利用ajaxfileupload插件实现文件上传无刷新的具体方法
2013/06/08 Javascript
jquery通过a标签删除table中的一行的代码
2013/12/02 Javascript
Jquery Ajax解析XML数据(同步及异步调用)简单实例
2014/02/12 Javascript
JavaScript中按位“异或”运算符使用介绍
2014/03/14 Javascript
JS实现Fisheye效果动感放大菜单代码
2015/10/21 Javascript
尝试动手制作javascript放大镜效果
2015/12/25 Javascript
jquery+json实现分页效果
2016/03/07 Javascript
微信小程序开发入门基础教程
2017/04/19 Javascript
神级程序员JavaScript300行代码搞定汉字转拼音
2017/05/20 Javascript
Vue 实现拖动滑块验证功能(只有css+js没有后台验证步骤)
2018/08/24 Javascript
vue.js 添加 fastclick的支持方法
2018/08/28 Javascript
vue 导航内容设置选中状态样式的例子
2019/11/01 Javascript
vue中改变滚动条样式的方法
2020/03/03 Javascript
详解react组件通讯方式(多种)
2020/05/06 Javascript
如何检测JavaScript中的死循环示例详解
2020/08/30 Javascript
vue3中轻松实现switch功能组件的全过程
2021/01/07 Vue.js
400多行Python代码实现了一个FTP服务器
2012/05/10 Python
Python使用numpy模块创建数组操作示例
2018/06/20 Python
Python列表常见操作详解(获取,增加,删除,修改,排序等)
2019/02/18 Python
Django model 中设置联合约束和联合索引的方法
2019/08/06 Python
利用anaconda作为python的依赖库管理方法
2019/08/13 Python
Django后端发送小程序微信模板消息示例(服务通知)
2019/12/17 Python
python 解决print数组/矩阵无法完整输出的问题
2020/02/19 Python
用于ETL的Python数据转换工具详解
2020/07/21 Python
用 python 进行微信好友信息分析
2020/11/28 Python
HTML5 新标签全部总汇(推荐)
2016/06/13 HTML / CSS
Python如何实现单例模式
2016/06/03 面试题
合作经营协议书范本
2014/09/16 职场文书
爱心捐款感谢信
2015/01/20 职场文书
Spring Data JPA框架Repository自定义实现
2022/04/28 Java/Android