1
This article records and imitates a el-collapse component details, which will help you better understand the specific working details of the corresponding components of Ele.me ui. This article is another article in the elementui source code learning and imitation series. I will continue to update and imitate other components when I am free. The source code is on github, you can pull it down, npm start to run, and comments are helpful for better understanding. The github warehouse address is as follows: https://github.com/shuirongshuifu/elementSrcCodeStudy

component thinking

el-collapse means the folding panel, which is generally used for: grouping and hiding complex areas, keeping the page neat and tidy, with the meaning of sorting.

collapse means folding, but fold also means folding. Therefore, the name of the package packaged by the author here is changed, not my-collapse , but my-fold

component requirements

Let's take a look at the structure diagram of the folding component in the figure below.

Combined with the work experience in the above figure, the requirements for the general analysis of the components are as follows:

  • Click on the collapsed header area to expand or close the collapsed content body area
  • When expanding or collapsing, add transition effects
  • Definition of the content text parameter of the header area
  • Whether to hide the folded small arrow
  • Accordion mode collapsible panel (all can be expanded and collapsed by default)

The parent component of the component implementation uniformly changes the state of all child components

In general, the parent component changes the data state of the child component in the following ways:

  1. The parent component passes data, and the child component props receive it. Change the parent component data, and the child component will automatically change and update
  2. Use this.$refs.child.xxx = yyy , type a ref for the subcomponent, and change the corresponding value directly
  3. Use this.$children to access all child component instance objects. Therefore, it can also be changed directly, as follows:

parent component code

 // html
<template>
  <div>
    <h2>下方为三个子组件</h2>
    <child1 />
    <child2 />
    <child3 />
    <button @click="changeChildData">点击按钮更改所有子组件数据</button>
  </div>
</template>
// js
changeChildData() {
  // this.$children拿到所有子组件实例对象的数组,遍历访问到数据,更改之
  this.$children.forEach((child) => {
    child.flag = !child.flag;
  });
},

One of the subcomponent code, the other two are the same

 // html
<template>
  <div>child1中的flag--> {{ flag }}</div>
</template>
// js
<script>
export default {
  data() { return { flag: false } },
};
</script>

renderings

Why mention this? Because the accordion mode collapsed panel will use this method to change other panels and make other panels close

The package of the highly transition effect component implemented by the component

The transition of height, mainly from 0 to a certain height, and from a certain height to 0, needs to be controlled with the transition and overflow attributes. Let's take a look at the simple writing method and renderings first, and then look at the code of the packaged component

1. Simple writing

Reach out to party benefits, copy and paste to use

 <!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .target {
            width: 120px;
            height: 120px;
            line-height: 120px;
            text-align: center;
            background-color: #baf;
            /* 以下两个是必要的样式控制属性 */
            transition: height 0.2s linear;
            overflow: hidden;
        }
    </style>
</head>

<body>
    <button>点击高度变化</button>
    <br>
    <br>
    <div class="target">过渡的dom</div>
    <script>
        let isOpen = true // 初始情况下,标识状态为打开状态
        let btn = document.querySelector('button')
        let targetDom = document.querySelector('.target')
        btn.onclick = () => {
            // 若为展开状态,就将其高度置为0,因为css有过渡代码,所以高度过渡效果就出来了
            if (isOpen) { 
                targetDom.style.height = 0 + 'px'
                isOpen = false
            } 
            // 若为关闭状态,就将其高度置为原来,因为css有过渡代码,所以高度过渡效果就出来了
            else { 
                targetDom.style.height = 120 + 'px'
                isOpen = true
            }
        }
    </script>
</body>

</html>

2. Simple writing renderings

高度过渡.gif

When we encapsulate the folding panel, this transition component with varying heights is a must. If not, when the folding panel is unfolded and closed, it will be a bit awkward, and adding a component will make it silky.

3. Encapsulation of folded components

Understand the above simple case, and then apply its ideas to component packaging.

Highly component encapsulation code ideas:

According to the identification of the show variable, change the dom.style.height ; When initial loading, get the initial height `dom
.offsetHeight 更改一次、当 show 变量标识发生变化的时候,再更改一次。同时搭配高度的 transition 样式控制即可(即:监听 props show`标识的变化更改之)

The encapsulated height transition component code is as follows:

 <template>
  <div class="transitionWrap" ref="transitionWrap">
    <slot></slot>
  </div>
</template>
 
<script>
export default {
  props: {
    // 布尔值show标识关闭还是展开
    show: Boolean,
  },
  data() {
    return {
      height: 0,
    };
  },
  mounted() {
    /* dom加载完毕,然后根据标识show去手动更新高度 */
    this.$nextTick(() => {
      this.height = this.$refs.transitionWrap.offsetHeight;
      this.$refs.transitionWrap.style.height = this.show
        ? `${this.height}px`
        : 0;
    });
    // this.$nextTick().then(() => { ... }
  },
  watch: {
    /* 再监听标识的变化,从而更改高度,即关闭还是展开 */ 
    show(newVal) {
      this.$refs.transitionWrap.style.height = newVal ? `${this.height}px` : 0;
    },
  },
};
</script>
 
<style scoped>
/* 关键css样式,高度线性匀速过渡 */
.transitionWrap {
  transition: height 0.2s linear;
  overflow: hidden;
}
</style>
In addition, the Ele.me UI also provides the el-collapse-transition component, which is also a good choice.

About the role attribute and aria-multiselectable etc. in the component

In fact, there are many things to consider when encapsulating a set of powerful open source components. For example, it needs to adapt to screen readers. Let's take a look at the Ele.me UI el-collapse two screen reader properties used by components role and aria-multiselectable . As shown below:

  • role attribute is a further addition to semantic tags in html (eg screen readers, for the blind), another example
  • <div role="checkbox" aria-checked="checked" /> Height screen reader, there is a checkbox here and it is already checked
  • aria-multiselectable='true' Tell the auxiliary device whether multiple items can be expanded at a time, or only one can be expanded

For details, see css god, Zhang Xinxu's blog post: https://www.zhangxinxu.com/wordpress/2012/03/wai-aria-%e6%97%a0%e9%9a%9c%e7%a2%8d%e9 %98%85%e8%af%bb/

It can be seen from this that a set of open source components must be considered in all aspects.

packaged components

Let's take a look at the renderings

Package renderings

Use your own encapsulated collapsible component

 <template>
  <div>
    <!-- 手风琴模式 -->
    <my-fold v-model="openArr" accordion @change="changeFn">
      <my-fold-item title="第一项" name="one">我是第一项的内容</my-fold-item>
      <my-fold-item title="第二项" name="two">
        <p>我是第二项的内容</p>
        <p>我是第二项的内容</p>
      </my-fold-item>
      <my-fold-item title="第三项" name="three">
        <p>我是第三项的内容</p>
        <p>我是第三项的内容</p>
        <p>我是第三项的内容</p>
      </my-fold-item>
      <my-fold-item title="第四项" name="four">
        <p>我是第四项的内容</p>
        <p>我是第四项的内容</p>
        <p>我是第四项的内容</p>
        <p>我是第四项的内容</p>
      </my-fold-item>
    </my-fold>
    <br />
    <!-- 可展开多个模式 -->
    <my-fold v-model="openArr2" @change="changeFn">
      <my-fold-item title="第一项" name="one">我是第一项的内容</my-fold-item>
      <my-fold-item title="第二项" name="two">
        <p>我是第二项的内容</p>
        <p>我是第二项的内容</p>
      </my-fold-item>
      <my-fold-item title="第三项" name="three">
        <p>我是第三项的内容</p>
        <p>我是第三项的内容</p>
        <p>我是第三项的内容</p>
      </my-fold-item>
      <my-fold-item title="第四项" name="four">
        <p>我是第四项的内容</p>
        <p>我是第四项的内容</p>
        <p>我是第四项的内容</p>
        <p>我是第四项的内容</p>
      </my-fold-item>
    </my-fold>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 手风琴模式的数组项要么没有项,要么只能有一个项
      openArr: [],
      // 可展开多个的数组,可以有多个项
      openArr2: ["one", "two"],
    };
  },
  methods: {
    changeFn(name, isOpen, vNode) {
      console.log(name, isOpen, vNode);
    },
  },
};
</script>

my-fold组件

 <template>
  <div class="myFoldWrap">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: "myFold",
  props: {
    // 是否开启手风琴模式(每次只能展开一个面板)
    accordion: {
      type: Boolean,
      default: false, // 默认不开启(可展开多个)
    },
    // 父组件v-model传参,子组件props中key为'value'接收,'input'事件更改
    value: {
      type: Array, // 手风琴模式的数组项只能有一个,反之可以有多个
      default() {
        return [];
      },
    },
  },
  data() {
    return {
      // 展开的项可一个,可多个(使用层v-model数组传的有谁,就展开谁)
      openArr: this.value, // 收集谁需要展开
    };
  },
  mounted() {
    // 手动加一个校验
    if (this.accordion & (this.value.length > 1)) {
      console.error("手风琴模式下,绑定的数组最多一项");
    }
  },
  watch: {
    // 监听props中value的变化,及时更新
    value(value) {
      this.openArr = value;
    },
  },
  methods: {
    updateVModel(name, isOpen, vNode) {
      // 若为手风琴模式
      if (this.accordion) {
        // 当某一项打开的时候,才去关闭其他项
        isOpen ? this.closeOther(name) : null;
        this.openArr = [name]; // 手风琴模式只保留(展开)一个
      }
      // 若为可展开多项模式
      else {
        let i = this.openArr.indexOf(name);
        // 包含就删掉、不包含就追加
        i > -1 ? this.openArr.splice(i, 1) : this.openArr.push(name);
      }
      // 无论那种模式,都需要更改并通知外层使用组件
      this.$emit("input", this.openArr); // input事件控制v-model的数据更改
      this.$emit("change", name, isOpen, vNode); // change事件抛出去,供用户使用
    },
    closeOther(name) {
      this.$children.forEach((item) => {
        // 将除了自身以外的都置为false,故其他的就都折叠上了
        if (item.name != name) {
          item.isOpen = false;
        }
      });
    },
  },
};
</script>

<style lang="less" scoped>
.myFoldWrap {
  border: 1px solid #e9e9e9;
}
</style>

my-fold-item Component

 <template>
  <div class="foldItem">
    <!-- 头部部分,主要是点击时展开内容,以及做小箭头的旋转,和头部的标题呈现 -->
    <div class="foldItemHeader" @click="handleHeaderClick">
      <i
        v-if="!hiddenArrow"
        class="el-icon-arrow-right"
        :class="{ rotate90deg: isOpen }"
      ></i>
      {{ title }}
    </div>
    <!-- 内容体部分,主要是展开折叠时加上高度过渡效果,这里封装了一个额外的工具组件 -->
    <transition-height class="transitionHeight" :show="isOpen">
      <div class="foldItemBody">
        <slot></slot>
      </div>
    </transition-height>
  </div>
</template>

<script>
import transitionHeight from "@/components/myUtils/transitionHeight/index.vue";
export default {
  name: "myFoldItem",
  components: {
    transitionHeight, // 高度过渡组件
  },
  props: {
    title: String, // 折叠面板的标题
    name: String, // 折叠面板的名字,即为唯一标识符(不可与其他重复!)
    // 是否隐藏小箭头,默认false,即展示小箭头
    hiddenArrow: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      // true为展开即open,false为折叠
      // 初始情况下取到父组件myFold组件的展开的数组,看看自身是否在其中
      isOpen: this.$parent.openArr.includes(this.name),
    };
  },
  methods: {
    // 点击展开或折叠
    handleHeaderClick() {
      this.isOpen = !this.isOpen; // 内容依托于变量isOpen直接更新即可
      this.$parent.updateVModel(this.name, this.isOpen, this); // 于此同时也要通知父组件去更新
    },
  },
};
</script>

<style lang="less" scoped>
.foldItem {
  width: 100%;
  height: auto; // 高度由内容区撑开
  .foldItemHeader {
    box-sizing: border-box;
    padding-left: 8px;
    min-height: 50px;
    display: flex;
    align-items: center;
    background-color: #fafafa;
    cursor: pointer;
    border-bottom: 1px solid #e9e9e9;
    // 展开折叠项时,小图标旋转效果
    i {
      transform: rotate(0deg);
      transition: all 0.24s;
      margin-right: 8px;
    }
    .rotate90deg {
      transform: rotate(90deg);
      transition: all 0.24s;
    }
  }
  .foldItemBody {
    width: 100%;
    height: auto;
    box-sizing: border-box;
    padding: 12px 12px 12px 8px;
    border-bottom: 1px solid #e9e9e9;
  }
}
// 去除和父组件的边框重叠
.foldItem:last-child .foldItemHeader {
  border-bottom: none !important;
}
.foldItem:last-child .transitionHeight .foldItemBody {
  border-top: 1px solid #e9e9e9;
  border-bottom: none !important;
}
</style>
The above code is combined with comments for better understanding. Of course, the full version of the code is on the github repository. If it can help you a little bit, you are welcome to wave your hand and give a star ^_^

水冗水孚
1.1k 声望588 粉丝

每一个不曾起舞的日子,都是对生命的辜负