小程序篇-tab组件

作者: 前端精 | 来源:发表于2018-10-25 16:17 被阅读272次

    如何编写小程序的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模版的那种不灵活的编写方式,虽然组件一些方法需要微信客户端更高版本,我们有时需要去兼容低版本微信,但是我们秉持拥抱高版本,拥抱新增功能的态度。
    ——尼古拉斯·峰

    相关文章

      网友评论

        本文标题:小程序篇-tab组件

        本文链接:https://www.haomeiwen.com/subject/tvhuzftx.html