小程序篇-tab组件

如何编写小程序的tab组件?

小讨论:我们都知道小程序可以用template编写一些模版,后来小程序又可以实现与vue类似组件的编写——Component构造器。但是个人觉得功能还是没有vue组件来得强大,不过实现一些平时用到的业务场景还是可以的。

下面我给大家来表演如何实现tab组件

tab的话就是上面一排标签,点击标签实现底下的面板进行切换显示,这个其实是不难实现的,但是我们要整点复杂的,才可以写在简书里
思路:借鉴了一些vue第三方组件的封装思路,我们让tab组件由tab和tab-panel两个组件以父子关系组成,然后我们根据tab-panel的一些属性和数量来生成tab,这就涉及到Component构造器父子组件之间的联系。

项目结构图
tab效果图

我们新建一个Component 名为tab
tab.js

Component({
  // 关联子组件
  relations: {
    '../tab-panel/tab-panel': {
      type: 'child',
      linked(target) {},
      linkChanged(target) {},
      unlinked(target) {}
    }
  },

  properties: {
    // 内联样式
    iStyle: {
      type: String,
      value: ''
    },
    // 用来初始化显示某个panel
    value: {
      type: String,
      value: ''
    },
    // tab标签数组
    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,用来生成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及一些元素的计算
     */
    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
          });
        })
        // 设置第一个tab元素的left
        .select('.first')
        .boundingClientRect(rect => {
          this.setData({ initMl: rect.left });
        })
        // 获取tab外层滚动的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事件,计算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 => {
          // 计算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 绑定滚动,判断是否滚动到最右侧来显示渐变蒙版
     */
    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 切换到某个面板
     */
    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有几个大属性组成,分别是relations【定义与子组件关系】,properties【父组件传递接收】,data【组件内部data】,ready【组件生命周期函数,在组件布局完成后执行,此时可以获取节点信息】,这里只用到所有生命周期中的ready,可查阅 组件的生命周期,methods【组件内部方法】。
this.getRelationNodes('../tab-panel/tab-panel') 我们有了小程序获取所有子组件这个方法的支持,让我们与子组件的操作更加便利。

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 实现tab标签多了之后可以进行滚动,而且在scroll-view可以加入平滑滚动效果,slot插槽是用来放置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%;
  /* ... */

这是实现右侧渐变蒙版的效果,可以看下我之前写的 css篇-mask-image + linear-gradient 优雅显示富文本过长

tab的父组件就这样完成了,接下来我们来看下子组件tab-panel的编写


我们新建一个Component 名为tab-panel
tab-panel.js

Component({
  relations: {
    '../tab/tab': {
      type: 'parent',
      linked(target) {},
      linkChanged(target) {},
      unlinked(target) {}
    }
  },
  properties: {
    // 内联样式
    iStyle: {
      type: String,
      value: ''
    },
    // label用来显示tab的标签名
    label: {
      type: String,
      value: ''
    },
    // name为panel的唯一标识,用来确定要显示哪个panel
    name: {
      type: String,
      value: ''
    }
  },
  data: {
    // 是否显示当前panel
    isShow: false
  }
});

tab-panel.wxml

<view class="tab-panel" style="display:{{isShow?'block':'none'}};{{iStyle}}">
  <slot></slot>
</view>

slot 插槽用来放置实际的内容

tab-panel.wxss

.tab-panel {
  box-sizing: border-box;
  padding: 0 30rpx 0;
}

css可以自己定义,根据需求

这样子我们就完成了tab-panel子组件了


我们可以看到主要的代码编写还是在tab.js里面,因为tab-panel 说白了就支持了tab显示需要的数组,接下我们们看看在index页面中如何调用这个组件。

index.js

Page({
  data: {},
  onLoad() {},
  // 子组件事件触发
  onChangeTab({ detail: { name } }) {
    console.log('name :', name);
  },
  // 跳转到制定panel
  toPanel({
    currentTarget: {
      dataset: { panelName }
    }
  }) {
    this.selectComponent('#tab').toPanel(panelName);
  }
});
/* 
这里的onChangeTab是子组件触发调用的,
类似vue中的$emit的用法,
this.selectComponent('#tab').toPanel(panelName) 为调用子组件方法,
类似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>第一个panel</view>
        <button bindtap="toPanel" data-panel-name="{{'panel6'}}">跳转到panel6</button>
      </view>
    </tab-panel>
    <tab-panel i-style="height:calc(100% - 86rpx);box-sizing:border-box;" label="我是panel2我比较长" name="panel2">
      <scroll-view class="index__scroll-view" scroll-y="{{true}}">
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
        <view>第二个panel</view>
      </scroll-view>
    </tab-panel>
    <tab-panel label="我是panel3" name="panel3">
      <view class="index__panel">第三个panel</view>
    </tab-panel>
    <tab-panel label="我是panel4" name="panel4">
      <view class="index__panel">第四个panel</view>
    </tab-panel>
    <tab-panel label="我是panel5" name="panel5">
      <view class="index__panel">第五个panel</view>
    </tab-panel>
    <tab-panel label="我是panel6" name="panel6">
      <view class="index__panel">
        <view class="index__panel">
          <view>第六个panel</view>
          <button bindtap="toPanel" data-panel-name="{{'panel1'}}">跳转到panel1</button>
        </view>
      </view>
    </tab-panel>
    <tab-panel label="我是panel7" name="panel7">
      <view class="index__panel">第七个panel</view>
    </tab-panel>
    <tab-panel label="我是panel8" name="panel8">
      <view class="index__panel">第八个panel</view>
    </tab-panel>
  </tab>
</view>

我们这里写了8个panel作为例子,tab-panel为自定义的内容,我们现在需要管理维护的就只是tab-panel里面的内容啦。

index.wxss

page {
  height: 100%;
}
.index {
  height: 100%;
}
.index__scroll-view {
  height: 100%;
  box-sizing: border-box;
  padding: 10rpx 0;
}

表演结束!

学会了组件的编写,我们可以舍弃template模版的那种不灵活的编写方式,虽然组件一些方法需要微信客户端更高版本,我们有时需要去兼容低版本微信,但是我们秉持拥抱高版本,拥抱新增功能的态度。
——尼古拉斯·峰

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容