如何編寫(xiě)小程序的tab組件?
小討論:我們都知道小程序可以用template編寫(xiě)一些模版缀辩,后來(lái)小程序又可以實(shí)現(xiàn)與vue類似組件的編寫(xiě)——Component構(gòu)造器踪央。但是個(gè)人覺(jué)得功能還是沒(méi)有vue組件來(lái)得強(qiáng)大,不過(guò)實(shí)現(xiàn)一些平時(shí)用到的業(yè)務(wù)場(chǎng)景還是可以的健无。
下面我給大家來(lái)表演如何實(shí)現(xiàn)tab組件
tab的話就是上面一排標(biāo)簽液斜,點(diǎn)擊標(biāo)簽實(shí)現(xiàn)底下的面板進(jìn)行切換顯示旗唁,這個(gè)其實(shí)是不難實(shí)現(xiàn)的,但是我們要整點(diǎn)復(fù)雜的,才可以寫(xiě)在簡(jiǎn)書(shū)里祷嘶。
思路:借鑒了一些vue第三方組件的封裝思路论巍,我們讓tab組件由tab和tab-panel兩個(gè)組件以父子關(guān)系組成嘉汰,然后我們根據(jù)tab-panel的一些屬性和數(shù)量來(lái)生成tab双泪,這就涉及到Component構(gòu)造器父子組件之間的聯(lián)系。
我們新建一個(gè)Component 名為tab
tab.js
Component({
// 關(guān)聯(lián)子組件
relations: {
'../tab-panel/tab-panel': {
type: 'child',
linked(target) {},
linkChanged(target) {},
unlinked(target) {}
}
},
properties: {
// 內(nèi)聯(lián)樣式
iStyle: {
type: String,
value: ''
},
// 用來(lái)初始化顯示某個(gè)panel
value: {
type: String,
value: ''
},
// tab標(biāo)簽數(shù)組
tab: {
type: Array,
value: []
}
},
data: {
selectIndex: 0,
tabIndex: 0,
scrollLeft: 0,
width: 0,
ml: 0,
initMl: 0,
svWidth: 0,
panelNodes: [],
isLower: false,
lastLeft: 0,
lastWidth: 0
},
ready() {
this.getAllPanel();
this.initCal();
},
methods: {
/**
* @desc 獲取子組件tab-panel,用來(lái)生成tab
*/
getAllPanel() {
const { value } = this.data;
const ttab = [];
const panelNodes = this.getRelationNodes('../tab-panel/tab-panel');
this.setData({ panelNodes });
panelNodes.map((item, i) => {
const {
data: { label, name }
} = item;
if (value === name) this.setData({ selectIndex: i });
ttab.push({ text: label });
});
this.setData({ tab: ttab });
},
/**
* @desc 初始化tab及一些元素的計(jì)算
*/
initCal() {
wx.createSelectorQuery()
.in(this)
.selectAll('.tab__item')
.boundingClientRect(rects => {
const { tab } = this.data;
tab.map((item, i) => {
if (i === tab.length - 1) {
this.setData({
lastLeft: rects[i].left,
lastWidth: rects[i].width
});
}
item.left = rects[i].left;
});
this.setData({
tab
});
})
// 設(shè)置第一個(gè)tab元素的left
.select('.first')
.boundingClientRect(rect => {
this.setData({ initMl: rect.left });
})
// 獲取tab外層滾動(dòng)的view的寬度
.select('.scroll-view')
.boundingClientRect(rect => {
this.setData({ svWidth: rect.width });
const { selectIndex, tab } = this.data;
this.changeTabFun(selectIndex, tab[selectIndex].left);
})
.exec();
},
/**
* @desc 切換tab事件
*/
changeTab({
currentTarget: {
dataset: { index, left }
}
}) {
if (this.data.tabIndex === index) return;
this.changeTabFun(index, left);
},
/**
* @desc 切換tab事件,計(jì)算scroll-view顯示位置
*/
changeTabFun(index, left) {
const { tab, initMl, svWidth, panelNodes } = this.data;
tab.map((item, i) => (item.active = i === index));
this.setData({ tab, tabIndex: index });
wx.createSelectorQuery()
.in(this)
.select('.active')
.boundingClientRect(rect => {
// 計(jì)算scrollleft
const sc = left - (svWidth - rect.width) / 2 - initMl;
this.setData({
width: rect.width,
scrollLeft: sc
});
// 延遲底部橫線切換效果
setTimeout(() => {
this.setData({
ml: left - initMl
});
}, 80);
})
.exec();
panelNodes.map((item, i) => {
item.setData({ isShow: index === i });
});
this.triggerEvent('changeTab', { name: panelNodes[index].data.name });
},
/**
* @desc 綁定滾動(dòng),判斷是否滾動(dòng)到最右側(cè)來(lái)顯示漸變蒙版
*/
bindscroll({ detail: { scrollLeft } }) {
const { svWidth, initMl, lastLeft, lastWidth } = this.data;
const l = Math.floor(lastLeft - svWidth + lastWidth - initMl);
if (scrollLeft >= l - 1) {
this.setData({ isLower: true });
} else {
this.setData({ isLower: false });
}
},
/**
* @desc 切換到某個(gè)面板
*/
toPanel(panelName) {
const { panelNodes } = this.data;
this.setData({ selectIndex: 0 });
panelNodes.map((item, i) => {
const {
data: { name }
} = item;
if (panelName === name) this.setData({ selectIndex: i });
});
this.initCal();
}
}
});
我們可以看到tab.js Component有幾個(gè)大屬性組成,分別是relations【定義與子組件關(guān)系】尚卫,properties【父組件傳遞接收】,data【組件內(nèi)部data】怎爵,ready【組件生命周期函數(shù),在組件布局完成后執(zhí)行芙委,此時(shí)可以獲取節(jié)點(diǎn)信息】,這里只用到所有生命周期中的ready侧啼,可查閱 組件的生命周期痊乾,methods【組件內(nèi)部方法】。
this.getRelationNodes('../tab-panel/tab-panel') 我們有了小程序獲取所有子組件這個(gè)方法的支持协饲,讓我們與子組件的操作更加便利描馅。
tab.wxml
<view class="tab" style="{{iStyle}}">
<view class="tab__scroll">
<view class="tab__scroll-wrapper {{isLower?'lower':''}}">
<scroll-view class="scroll-view" scroll-x="{{true}}" scroll-with-animation="{{true}}" bindscroll="bindscroll" scroll-left="{{scrollLeft}}">
<view class="tab__list">
<view class="tab__item {{index===0?'first':''}} {{index===tab.length-1?'last':''}} {{item.active?'active':''}}" wx:for="{{tab}}" wx:key="{{index}}" bindtap="changeTab" data-index="{{index}}" data-left="{{item.left}}">
{{item.text}}
</view>
</view>
<view class="tab__line transition" style="width:{{width}}px; margin-left:{{ml}}px;"></view>
</scroll-view>
</view>
</view>
<slot></slot>
</view>
加入了scroll-view 實(shí)現(xiàn)tab標(biāo)簽多了之后可以進(jìn)行滾動(dòng)嘹狞,而且在scroll-view可以加入平滑滾動(dòng)效果,slot插槽是用來(lái)放置tab-panel組件的
tab.wxss
.tab__scroll {
padding: 20rpx 30rpx 0rpx;
font-size: 28rpx;
color: #9b9b9b;
position: relative;
}
.tab__scroll-wrapper {
border-bottom: 1rpx solid #f0f0f0;
-webkit-mask-image: linear-gradient(to right, #1a1a1a 80%, transparent);
mask-image: linear-gradient(to right, #1a1a1a 80%, transparent);
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
.tab__scroll-wrapper.lower {
-webkit-mask-image: linear-gradient(#1a1a1a 100%, transparent);
mask-image: linear-gradient(#1a1a1a 100%, transparent);
}
.tab__list {
white-space: nowrap;
width: 100%;
}
.tab__item {
vertical-align: top;
display: inline-block;
margin-right: 40rpx;
padding-top: 10rpx;
padding-bottom: 5rpx;
padding-left: 5rpx;
padding-right: 5rpx;
}
.tab__item:last-child {
margin-right: 0;
}
.tab__item.active {
color: #383538;
}
.tab__line {
width: 56rpx;
height: 4rpx;
background: #ffe700;
border-radius: 4rpx;
}
.tab .transition {
transition: all 0.3s ease 0s;
}
我們可以看到wxss有這樣的樣式
/* ... */
-webkit-mask-image: linear-gradient(to right, #1a1a1a 80%, transparent);
mask-image: linear-gradient(to right, #1a1a1a 80%, transparent);
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
/* ... */
這是實(shí)現(xiàn)右側(cè)漸變蒙版的效果,可以看下我之前寫(xiě)的 css篇-mask-image + linear-gradient 優(yōu)雅顯示富文本過(guò)長(zhǎng)
tab的父組件就這樣完成了,接下來(lái)我們來(lái)看下子組件tab-panel的編寫(xiě)
我們新建一個(gè)Component 名為tab-panel
tab-panel.js
Component({
relations: {
'../tab/tab': {
type: 'parent',
linked(target) {},
linkChanged(target) {},
unlinked(target) {}
}
},
properties: {
// 內(nèi)聯(lián)樣式
iStyle: {
type: String,
value: ''
},
// label用來(lái)顯示tab的標(biāo)簽名
label: {
type: String,
value: ''
},
// name為panel的唯一標(biāo)識(shí),用來(lái)確定要顯示哪個(gè)panel
name: {
type: String,
value: ''
}
},
data: {
// 是否顯示當(dāng)前panel
isShow: false
}
});
tab-panel.wxml
<view class="tab-panel" style="display:{{isShow?'block':'none'}};{{iStyle}}">
<slot></slot>
</view>
slot 插槽用來(lái)放置實(shí)際的內(nèi)容
tab-panel.wxss
.tab-panel {
box-sizing: border-box;
padding: 0 30rpx 0;
}
css可以自己定義,根據(jù)需求
這樣子我們就完成了tab-panel子組件了
我們可以看到主要的代碼編寫(xiě)還是在tab.js里面,因?yàn)閠ab-panel 說(shuō)白了就支持了tab顯示需要的數(shù)組绿语,接下我們們看看在index頁(yè)面中如何調(diào)用這個(gè)組件岗仑。
index.js
Page({
data: {},
onLoad() {},
// 子組件事件觸發(fā)
onChangeTab({ detail: { name } }) {
console.log('name :', name);
},
// 跳轉(zhuǎn)到制定panel
toPanel({
currentTarget: {
dataset: { panelName }
}
}) {
this.selectComponent('#tab').toPanel(panelName);
}
});
/*
這里的onChangeTab是子組件觸發(fā)調(diào)用的荠雕,
類似vue中的$emit的用法既鞠,
this.selectComponent('#tab').toPanel(panelName) 為調(diào)用子組件方法蚯姆,
類似vue中的this.$refs['xxx'].func()
*/
index.json
{
"usingComponents": {
"tab": "../../components/tab/tab/tab",
"tab-panel": "../../components/tab/tab-panel/tab-panel"
}
}
指定使用的組件 tab、tab-panel
index.wxml
<view class="index">
<tab i-style="height:100%;" id="tab" value="panel2" bind:changeTab="onChangeTab">
<tab-panel label="我是panel1" name="panel1">
<view class="index__panel">
<view>第一個(gè)panel</view>
<button bindtap="toPanel" data-panel-name="{{'panel6'}}">跳轉(zhuǎn)到panel6</button>
</view>
</tab-panel>
<tab-panel i-style="height:calc(100% - 86rpx);box-sizing:border-box;" label="我是panel2我比較長(zhǎng)" name="panel2">
<scroll-view class="index__scroll-view" scroll-y="{{true}}">
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
<view>第二個(gè)panel</view>
</scroll-view>
</tab-panel>
<tab-panel label="我是panel3" name="panel3">
<view class="index__panel">第三個(gè)panel</view>
</tab-panel>
<tab-panel label="我是panel4" name="panel4">
<view class="index__panel">第四個(gè)panel</view>
</tab-panel>
<tab-panel label="我是panel5" name="panel5">
<view class="index__panel">第五個(gè)panel</view>
</tab-panel>
<tab-panel label="我是panel6" name="panel6">
<view class="index__panel">
<view class="index__panel">
<view>第六個(gè)panel</view>
<button bindtap="toPanel" data-panel-name="{{'panel1'}}">跳轉(zhuǎn)到panel1</button>
</view>
</view>
</tab-panel>
<tab-panel label="我是panel7" name="panel7">
<view class="index__panel">第七個(gè)panel</view>
</tab-panel>
<tab-panel label="我是panel8" name="panel8">
<view class="index__panel">第八個(gè)panel</view>
</tab-panel>
</tab>
</view>
我們這里寫(xiě)了8個(gè)panel作為例子煮落,tab-panel為自定義的內(nèi)容蝉仇,我們現(xiàn)在需要管理維護(hù)的就只是tab-panel里面的內(nèi)容啦轿衔。
index.wxss
page {
height: 100%;
}
.index {
height: 100%;
}
.index__scroll-view {
height: 100%;
box-sizing: border-box;
padding: 10rpx 0;
}
表演結(jié)束蛤育!
學(xué)會(huì)了組件的編寫(xiě)瓦糕,我們可以舍棄template模版的那種不靈活的編寫(xiě)方式亥揖,雖然組件一些方法需要微信客戶端更高版本费变,我們有時(shí)需要去兼容低版本微信胡控,但是我們秉持擁抱高版本庇绽,擁抱新增功能的態(tài)度。
——尼古拉斯·峰