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 showModalDialog模态对话框使用说明
Dec 31 Javascript
当json键为数字时的取值方法解析
Nov 15 Javascript
鼠标滑在标题上显示图片的JS代码
Nov 19 Javascript
Firefox中使用outerHTML的2种解决方法
Jun 07 Javascript
jquery专业的导航菜单特效代码分享
Aug 29 Javascript
jQuery.ajax 跨域请求webapi设置headers的解决方案
Aug 08 Javascript
Bootstrap Table使用整理(一)
Jun 09 Javascript
微信小程序onLaunch异步,首页onLoad先执行?
Sep 20 Javascript
在vue中解决提示警告 for循环报错的方法
Sep 28 Javascript
简单了解Javscript中兄弟ifream的方法调用
Jun 17 Javascript
JS localStorage存储对象,sessionStorage存储数组对象操作示例
Feb 15 Javascript
详解vue beforeEach 死循环问题解决方法
Feb 25 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简单的伪原创程序,配合商城采集用的
2010/10/12 PHP
解析使用ThinkPHP应该掌握的调试手段
2013/06/20 PHP
PHP+jQuery 注册模块的改进(三):更新到Smarty3.1
2014/10/14 PHP
Jquery Change与bind事件代码
2011/09/29 Javascript
jquery选择器大全 全面详解jquery选择器
2014/03/06 Javascript
javascript感应鼠标图片透明度显示的方法
2015/02/24 Javascript
Bootstrap 网站实例之单页营销网站
2016/10/20 Javascript
AngularJS学习笔记(三)数据双向绑定的简单实例
2016/11/08 Javascript
详解Vue2.0里过滤器容易踩到的坑
2017/06/01 Javascript
微信小程序“摇一摇”的实例代码
2017/07/20 Javascript
nodejs实现简单的gulp打包
2017/12/21 NodeJs
微信小程序使用Promise简化回调
2018/02/06 Javascript
JavaScript 有用的代码片段和 trick
2018/02/22 Javascript
vue.js做一个简单的编辑菜谱功能
2018/05/08 Javascript
详解vue-cli 构建项目 vue-cli请求后台接口 vue-cli使用axios、sass、swiper
2018/05/28 Javascript
Vue数据绑定实例写法
2019/08/06 Javascript
详解微信小程序中var、let、const用法与区别
2020/01/11 Javascript
ant-design-vue按需加载的坑的解决
2020/05/14 Javascript
javascript递归函数定义和用法示例分析
2020/07/22 Javascript
在vue中使用jsonp进行跨域请求接口操作
2020/10/29 Javascript
[16:27]DOTA2 HEROS教学视频教你分分钟做大人-艾欧
2014/06/11 DOTA
python回溯法实现数组全排列输出实例分析
2015/03/17 Python
python实现井字棋游戏
2020/03/30 Python
深度定制Python的Flask框架开发环境的一些技巧总结
2016/07/12 Python
python使用for循环计算0-100的整数的和方法
2019/02/01 Python
Python后台开发Django的教程详解(启动)
2019/04/08 Python
jupyter notebook 中输出pyecharts图实例
2020/04/23 Python
python multiprocessing多进程变量共享与加锁的实现
2019/10/02 Python
Django实现将一个字典传到前端显示出来
2020/04/03 Python
深入解读CSS3中transform变换模型的渲染
2016/05/27 HTML / CSS
十岁生日同学答谢词
2014/01/19 职场文书
坚定理想信念心得体会
2014/03/11 职场文书
生日主持词
2014/03/20 职场文书
土建施工员岗位职责
2015/04/11 职场文书
新郎父母婚礼致辞
2015/07/27 职场文书
中秋节英文祝福语句(14句)
2019/09/11 职场文书