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 相关文章推荐
web页面数据展示新想法(json)
Jun 08 Javascript
我的javascript 函数链之演变
Apr 07 Javascript
javascript实现在下拉列表中显示多级树形菜单的方法
Aug 12 Javascript
jQuery插件实现静态HTML验证码校验
Nov 06 Javascript
基于gulp合并压缩Seajs模块的方式说明
Jun 14 Javascript
jQuery解析XML 详解及方法总结
Sep 28 Javascript
javascript实现秒表计时器的制作方法
Feb 16 Javascript
浅谈Vue.js
Mar 02 Javascript
vue实现点击图片放大效果
Aug 15 Javascript
深入理解Angular4订阅(Subscribe)与取消
Nov 22 Javascript
vue-cli脚手架-bulid下的配置文件
Mar 27 Javascript
微信小程序单选radio及多选checkbox按钮用法示例
Apr 30 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文本操作类
2006/11/25 PHP
强烈声明: 不要使用(include/require)_once
2013/06/06 PHP
php图片上传类 附调用方法
2016/05/15 PHP
php 多文件上传的实现实例
2016/10/23 PHP
laravel ORM 只开启created_at的几种方法总结
2018/01/29 PHP
javascript显示选择目录对话框的代码
2008/11/10 Javascript
Jsonp post 跨域方案
2015/07/06 Javascript
JS键盘版计算器的制作方法
2016/12/03 Javascript
如何理解jQuery中的ajaxSubmit方法
2017/03/13 Javascript
vue2 中如何实现动态表单增删改查实例
2017/06/09 Javascript
mpvue中使用flyjs全局拦截的实现代码
2018/09/13 Javascript
深入理解Angularjs 脏值检测
2018/10/12 Javascript
Vue传参一箩筐(页面、组件)
2019/04/04 Javascript
微信小程序点击顶部导航栏切换样式代码实例
2019/11/12 Javascript
Vue实现boradcast和dispatch的示例
2020/11/13 Javascript
Python httplib,smtplib使用方法
2008/09/06 Python
python字典get()方法用法分析
2015/04/17 Python
使用Python脚本来获取Cisco设备信息的示例
2015/05/04 Python
对Python生成器、装饰器、递归的使用详解
2019/07/19 Python
Python 转换文本编码实现解析
2019/08/27 Python
python验证码图片处理(二值化)
2019/11/01 Python
使用wxpy实现自动发送微信消息功能
2020/02/28 Python
Python中内建模块collections如何使用
2020/05/27 Python
细说NumPy数组的四种乘法的使用
2020/12/18 Python
使用HTML5 Canvas API中的clip()方法裁剪区域图像
2016/03/25 HTML / CSS
英国Amara家居法国网站:家居装饰,现代装饰和豪华礼品
2016/12/15 全球购物
比利时香水网上商店:NOTINO
2018/03/28 全球购物
家得宝墨西哥官网:The Home Depot墨西哥
2019/11/18 全球购物
你经历的项目中的SCM配置项主要有哪些?什么是配置项?
2013/11/04 面试题
生物科学专业个人求职信范文
2013/12/05 职场文书
岗位职责风险点
2014/03/12 职场文书
解除租房协议书
2014/12/03 职场文书
幼儿园老师工作总结2015
2015/05/22 职场文书
golang 如何用反射reflect操作结构体
2021/04/28 Golang
教你怎么用Python生成九宫格照片
2021/05/20 Python
海贼王十大逆天果实 魂魂果实上榜,岩浆果实攻击力最强
2022/03/18 日漫