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 ...

April 22, 2019 · 5 min · jiezi

实现tab标签下选中条滑动效果-react组件

这个是模仿ant design的Tabs控件,当切换tab时,下面的蓝色条滑过的效果。点击查看效果我只是封装了tab的头部标签,并没有包含内容部分。我的最终结果相关技术transformtransitiontransformtransform属性允许你旋转,缩放,倾斜或平移给定元素。这是通过修改CSS视觉格式化模型的坐标空间来实现的。通过看ant design的代码,他使用的是translate3d平移函数。transform: translate3d(100px, 0px, 0px)translate3d函数translate3d() 这个CSS 函数用于移动元素在3D空间中的位置。 这种变换的特点是三维矢量的坐标定义了它在每个方向上的移动量。语法translate3d(tx, ty, tz)transitiontransition控制滑动速度及滑动时间等。不用这个属性,效果没那么自然。官方解释:transitionCSS属性是一个简写属性,用于transition-property,transition-duration,transition-timing-function 和 transition-delay。上面就是实现需要用到的比较不常见的技术,所以专门列举出来。布局分析首先一个外层的div当做容器headerContainer里面分为上下两部分,上面就是包含各个“标签”的容器header,下面是滑动条tab_bar注意:滑动条是一个专门的div来实现,并不是“标签”容器的下边框标签容器里面放各个“标签”元素代码布局如下:<div class=“headerContainer”> <div class=“header”> <div class=“headItem”>tab 1</div> <div class=“headItemChecked”>tab 2</div> </div> <div class=“tab_bar”></div></div>说明headerContainer目前没有需要的css,由于我是用less写的,只是用它当做一个容器来用。header和headItemChecked设置标签的排列方式、字体等样式tab_bar的css样式是关键,它设置选中条的样式,值得注意的是,它需要和标签的状态和宽度保持一致。如何和标签状态保持一致当选中一个标签时,“选中条”需要滑动到对应的标签下面。这个通过设置“选中条”的平移位置来实现,这个可以设置translate3d的参数来实现translate3d(${checkedPosition}px, 0px, 0px)当点击标签时,动态设置checkedPosition的值即可。 onClickHeader = (checkedHead, index) => { this.setState({ checkedHead, checkedPosition: index * 100 }); };但是这时虽然能滑过去,但是没有那种平滑的滑动效果,实现这个效果就需要transition来实现。transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), -webkit-transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);这个和ant design的参数保持一致但是每个标签的内容导致宽度是不一样的,所以不能乘以换一个固定的值(100)来计算每次平移的位置,需要每个标签的实际宽度来决定平移的位置。计算每个标签的宽度这个就用到了react不推荐使用的ref属性了。这里推荐使用回调函数的方式,不然eslint会警告你⚠️,当然你没用eslint就无所谓了。通过ref来获取元素的宽度,然后计算容器的宽度、选中条的宽度和位置。 <div ref={r => { this[ref_${index}] = r; }} key={item.code}….省略部分//计算宽度onClickHeader = (checkedHead, index) => { const preWidth = index > 0 ? this[ref_${index - 1}].offsetWidth : 0; const barWidth = this[ref_${index}].offsetWidth; this.setState({ checkedHead, checkedPosition: index * preWidth, barWidth });};解决offsetWidth四舍五入的问题offsetWidth虽然能获取元素的宽度,但是在使用过程中发现,它返回的都是整数,进行了四舍五入的情况,当宽度遇到小于0.5的情况,就会引起内容换行了,很不美观,所以不能使用offsetWidth.解决方法如下:Element.getBoundingClientRect()Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。this[ref_${index}].getBoundingClientRect().width;//192.243返回的是包含小数的数字,比如192.243Window.getComputedStyle()Window.getComputedStyle()方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有CSS属性的值。 私有的CSS属性值可以通过对象提供的API或通过简单地使用CSS属性名称进行索引来访问。getComputedStyle(this[ref_${index}], null).getPropertyValue(‘width’);//192.243px返回的是带单位(px)的值,比如192.243px。由于涉及到计算,我上面使用了第一种解决方法。完整codeindex.jsimport React, { PureComponent } from ‘react’;import styles from ‘./index.less’;export default class TabHeader extends PureComponent { constructor(props) { super(props); const { defaultHead } = this.props; this.state = { containerWidth: 1500, checkedPosition: 0, barWidth: 70, checkedHead: defaultHead, }; } componentDidMount() { const { heardList } = this.props; let containerWidth = 0; (heardList || []).forEach((item, index) => { containerWidth += this[ref_${index}].getBoundingClientRect().width; }); this.setState({ barWidth: this.ref_0.getBoundingClientRect().width, containerWidth }); } onClickHeader = (checkedHead, index) => { let preWidth = 0; for (let i = 0; i < index; i += 1) { preWidth += this[ref_${i}].offsetWidth; } const barWidth = this[ref_${index}].offsetWidth; this.setState({ checkedHead, checkedPosition: preWidth, barWidth }); }; render() { const { checkedHead, checkedPosition, containerWidth, barWidth } = this.state; const { heardList, source } = this.props; return ( <div className={styles.container} style={{ width: ${containerWidth}px }}> <div className={styles.headerContainer}> <div className={styles.header}> {heardList.map((item, index) => ( <div ref={r => { this[ref_${index}] = r; }} key={item.code} className={checkedHead === item.code ? styles.headItemChecked : styles.headItem} onClick={this.onClickHeader.bind(this, item.code, index)} > {item.text} {item.num} </div> ))} </div> <div className={styles.tab_bar} style={{ transform: translate3d(${checkedPosition}px, 0px, 0px), width: ${barWidth}px, }} /> </div> <div className={styles.list}> {source.map(item => ( <div key={item.id} className={styles.row}> <div> <div className={styles.name}>{item.name}</div> <div className={styles.phone}>{item.phone}</div> </div> <div className={styles.count}> <span className={styles.doing}>{item.doing}</span> / <span className={styles.error}> {item.error}</span> / <span className={styles.all}> {item.all}</span> </div> </div> ))} </div> </div> ); }}index.less.container { background: rgba(255, 255, 255, 1); box-sizing: border-box; .headerContainer { position: relative; box-sizing: border-box; .header { display: flex; box-sizing: border-box; .headItem { box-sizing: border-box; font-size: 14px; font-family: PingFangSC-Regular; font-weight: 400; color: rgba(51, 51, 51, 1); text-align: center; padding: 12px 17px; cursor: pointer; border-bottom: 4px solid rgba(232, 232, 232, 1); transition: border 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); } .headItemChecked { .headItem; color: rgba(0, 155, 255, 1); } } .tab_bar { position: absolute; bottom: 0px; box-sizing: border-box; background-color: #1890ff; height: 4px; transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), -webkit-transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); } } .list { .row { display: flex; justify-content: space-between; padding: 14px 15px 10px 8px; font-size: 12px; font-family: PingFangSC-Regular; font-weight: 400; color: rgba(102, 102, 102, 1); border-bottom: 1px solid rgba(232, 232, 232, 1); .name { font-size: 14px; font-weight: 600; color: rgba(51, 51, 51, 1); } .phone { margin-top: 15px; } .count { font-size: 14px; font-family: PingFangSC-Semibold; font-weight: 600; .doing { color: rgba(24, 137, 250, 1); } .error { color: #EB9E08; } .all { color: #5f636b; } } } }}调用demo<TabHeader defaultHead=“abc” heardList={[ { code: ‘abc’, text: ‘较长的名字数量’, num: ‘10’ }, { code: ‘abcd’, text: ‘男人’, num: ‘101’ }, { code: ‘abce’, text: ‘美女数’, num: ‘121’ }, ]} source={[ { id: ‘12121’, name: ‘刘医生’, phone: ‘16807656551’, doing: ‘10’, error: ‘212’, all: ‘32’, }, { id: ‘1211’, name: ‘张无忌’, phone: ‘16807656551’, doing: ‘10’, error: ‘22’, all: ‘322’, }, ]} />如果对你有帮助,欢迎 (点赞 ) 和 (收藏) 给予鼓励。 ...

April 13, 2019 · 3 min · jiezi