实现vue折叠面板Collapse组件

4,870 阅读1分钟

概述

开发中,某些时候会用到将内容区域折叠/展开面板,根据多数组件库的Collapse组件的使用,来自己实现一个Collapse组件,这个组件还和组件库中的menu组件的实现类似,实现这个组件对于menu组件的实现,差别就不大了。

最终效果

动图有些大,稍等下就能加载出来 动画.gif

实现功能点和原理

  • 通过多数组件库的调研,Collapse组件需要提供的功能主要包括:
    • 菜单的折叠动画效果
    • 具备手风琴和普通的折叠效果
    • 能监听到展开项的变化
  • 组件的结构
    • Collapse包含Collapse组件和Panel组件,一个外层包裹容器,一个菜单项组件。
    • 对于折叠动画的实现我们需要手动自己封装transion组件,来实现元素隐藏的过渡效果(这里可以参阅vue-design组件库和element),他们对于组件折叠的过渡效果动画封装思路一致。

具体实现

文件结构

image.png

TransitionCollapse.vue

这个组件主要增强内置组件transition的功能,实现多数菜单折叠的过度

<template>
  <transition
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:after-enter="afterEnter"
    v-on:before-leave="beforeLeave"
    v-on:leave="leave"
    v-on:after-leave="afterLeave"
  >
    <slot></slot>
  </transition>
</template>

<script>
export default {
  methods: {
    beforeEnter(el) {
      el.classList.add("collapse-transition");
      el.dataset.oldPaddingTop = el.style.paddingTop;
      el.dataset.oldPaddingBottom = el.style.paddingBottom;
      el.dataset.oldOverflow = el.style.overflow;
      el.style.overflow = "hidden";
      el.style.height = "0";
      el.style.paddingTop = 0;
      el.style.paddingBottom = 0;
    },
    enter(el) {
      el.style.height = el.scrollHeight + "px";
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    },

    afterEnter(el) {
      el.classList.remove("collapse-transition");
      el.style.height = "";
      el.style.overflow = el.dataset.oldOverflow;
    },

    beforeLeave(el) {
      el.dataset.oldPaddingTop = el.style.paddingTop;
      el.dataset.oldPaddingBottom = el.style.paddingBottom;
      el.dataset.oldOverflow = el.style.overflow;
      el.style.height = el.scrollHeight + "px";
      el.style.overflow = "hidden";
    },

    leave(el) {
      el.classList.add("collapse-transition");
      el.style.height = 0;
      el.style.paddingTop = 0;
      el.style.paddingBottom = 0;
    },

    afterLeave(el) {
      el.classList.remove("collapse-transition");
      el.style.height = "";
      el.style.overflow = el.dataset.oldOverflow;
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    },
  },
};
</script>

<style lang="less">
.collapse-transition {
  transition: all 0.3s ease-in-out;
}
</style>

Collapse.vue

<template>
  <div class="g-collapse">
    <slot></slot>
  </div>
</template>

<script>
export default {
  props: {
    // 是否手风琴效果
    accordion: {
      type: Boolean,
      default: false,
    },
    // v-model绑定的值,具体展开哪一个面板
    value: {
      type: [String, Array],
    },
  },
  // 将当前实例注入到子组件
  provide() {
    return {
      collapseInstace: this,
    };
  },
  data() {
    return {
      // panel组件实例数组
      childrenVnode: [],
      //选中的value集合
      activeList: [],
    };
  },
  methods: {
    // 每项点击的时候,处理函数
    toggle(panelInstance) {
      // 如果是手风琴开启,那么只展开当前项,其他的关闭
      if (this.accordion) {
        if (this.activeList.indexOf(panelInstance.name) > -1) {
          this.activeList = [];
        } else {
          this.activeList = [panelInstance.name];
        }
      } else {
        // 处理手风琴关闭的时候,点击的时候将状态置反
        if (this.activeList.indexOf(panelInstance.name) == -1) {
          this.activeList.push(panelInstance.name);
        } else {
          for (let i = 0; i < this.activeList.length; i++) {
            if (this.activeList[i] == panelInstance.name) {
              this.activeList.splice(i, 1);
              break;
            }
          }
        }
      }
      // 发布事件处理函数,便于用户检测当前活跃项变更(v-mode和@change)
      this.$emit("input", this.activeList);
      this.$emit("change", this.activeList);
    },
    // 设置子组件的折叠状态
    setPanelInstanceStauts() {
      const panelInstanceComponent = this.childrenVnode;
      for (let i = 0; i < panelInstanceComponent.length; i++) {
        if (this.activeList.indexOf(panelInstanceComponent[i].child.name) > -1) {
          panelInstanceComponent[i].child.status = true;
        } else {
          panelInstanceComponent[i].child.status = false;

        }
      }
    },
    // 设置活跃集合
    setValueActiveList() {
      if (!Array.isArray(this.value)) {
        this.activeList = [this.value];
      }
    },
  },
  mounted() {
    // 保存子组件的实例
    this.childrenVnode = this.$slots.default;
    this.setValueActiveList();
  },
  watch: {
    // value变化的时候,将其转化成数组形式
    value(){
      this.setValueActiveList();
    },
    // 活跃项变化,设置子菜单折叠状态
    activeList() {
      this.setPanelInstanceStauts();
    },
  },
};
</script>

<style lang="less">
.g-collapse {
  border-bottom: 1px solid #dcdee2;
}
</style>

Panel.vue

<template>
  <div class="g-collapse-panel-item" @click.stop="handlePanelItemClick">
    <div class="title">
      <i :class="['arrow', status ? 'arrow-open' : '']"></i>
      <slot></slot>
    </div>
    <transition-collapse>
      <div class="content" v-show="status">
        <div class="inner-content">
          <slot name="content"></slot>
        </div>
      </div>
    </transition-collapse>
  </div>
</template>

<script>
import { TransitionCollapse } from "../TransitionCollapse";
export default {
  // 父组件实例
  inject: ["collapseInstace"],
  props: {
    // 当前面板标识
    name: {
      type: [String, Number],
    },
  },
  data() {
    return {
      status: false,
    };
  },
  components: { TransitionCollapse },
  methods: {
    // 点击折叠状态交给父组件处理
    handlePanelItemClick() {
      this.$parent.toggle(this);
    },
    // 根据初始状态的value,设置展开项
    init() {
      if (this.name == this.collapseInstace.value) {
        this.status = true;
      }
    },
  },
  mounted() {
    // 初始化
    this.init();
  },
};
</script>

<style lang="less">
.g-collapse-panel-item {
  margin-bottom: -1px;
  .title {
    height: 38px;
    line-height: 38px;
    padding-left: 36px;
    color: #666;
    cursor: pointer;
    position: relative;
    border: 1px solid #dcdee2;
    background-color: #f7f7f7;
  }
  .content {
    padding: 16px 36px;
    border-left: 1px solid #dcdee2;
    border-right: 1px solid #dcdee2;
  }
  .arrow {
    width: 10px;
    height: 10px;
    border: 1px solid #666;
    border-left: none;
    border-bottom: none;
    left: 10px;
    top: 50%;
    transform: translateY(-50%) rotate(45deg);
    transform-origin: center center;
    transition: transform 0.3s;
    position: absolute;
  }
  .arrow-open {
    transform: translateY(-50%) rotate(135deg);
  }
}
</style>

index.js

按需导出组件

import Collapse from "./Collapse.vue";
import Panel from "./Panel.vue";
import TransitionCollapse from "./TransitionCollapse.vue";
export { Panel, Collapse,TransitionCollapse };

总结

对于Collapse的实现,有助于对vue中transition组件的理解,同时,对于后续menu组件的封装,我们可以打好基础知识。