Vue render函数实战之实现tabs选项卡组件
用过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>
问题:
- 如何根据<el-tab-pane>来生成标签页?
- 如何过滤<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
下一篇:详解无限滚动插件vue-infinite-scroll源码解析
本文标题:Vue render函数实战之实现tabs选项卡组件
本文地址:http://www.codeinn.net/misctech/68889.html