Vue render函数实战之实现tabs选项卡组件
程序员文章站
2023-12-15 19:19:58
用过element ui库的童鞋肯定知道组件,简单、好用、可以自定义标签页,不知道广大童鞋们在刚开始使用组件的时...
用过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 render函数实战之实现tabs选项卡组件,希望对大家有所帮助