您好,登錄后才能下訂單哦!
用過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>
問題:
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選項卡組件,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網站的支持!
如果你覺得本文對你有幫助,歡迎轉載,煩請注明出處,謝謝!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。