本篇文章记录仿写一个el-collapse组件细节,从而有助于大家更好了解饿了么ui对应组件具体工作细节。本文是elementui源码学习仿写系列的又一篇文章,后续闲暇了会不断更新并仿写其余组件。源码在github上,大家能够拉下来,npm start运行跑起来,联合正文有助于更好的了解。github仓库地址如下:https://github.com/shuirongsh...

组件思考

el-collapse即为折叠面板的意思,个别次要是用于:对简单区域进行分组和暗藏,放弃页面的整洁,有分类整理的意思。

collapse有折叠的意思,不过fold也有折叠的意思。所以笔者这里封装的组件就改名字了,不叫my-collapse,叫做my-fold

组件的需要

咱们先看一下下图折叠组件的结构图

联合上图曾经工作教训,大抵剖析组件的需要有以下:

  • 点击折叠头部区域开展或敞开折叠内容体区域
  • 开展或折叠的时候,加上过渡成果
  • 头部区域的内容文字参数定义
  • 是否暗藏折叠的小箭头
  • 手风琴模式的折叠面板(默认是都能够开展折叠的)

组件实现之父组件对立更改所有子组件状态

个别状况下父组件更改子组件数据状态有以下形式:

  1. 父组件传递数据,子组件props接管。更改父组件数据,子组件也就主动更改更新了
  2. 应用this.$refs.child.xxx = yyy,给子组件打一个ref,间接更改对应值即可
  3. 应用this.$children能够拜访所有的子组件实例对象。所以,也能够间接更改,如下:

父组件代码

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

其中一个子组件代码,另外两个也一样

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

效果图

为什么要提到这个呢?因为手风琴模式下的折叠面板会用到这个形式去更改别的面板,使别的面板敞开

组件实现之高度过渡成果组件的封装

高度的过渡,次要是从0到某个高度,以及从某个高度到0的变动,须要搭配transition以及overflow属性去管制。咱们先看一下简略的写法和效果图,再看一下封装的组件的代码

1.简略写法

伸手党福利,复制粘贴即可应用

<!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.简略写法效果图

在咱们封装折叠面板的时候,这个高度变动的过渡组件是必须要有的,没有的话,折叠面板开展敞开时,会有点突兀,加上一个组件,会丝滑不少。

3.折叠组件的封装

了解了上述的简略案例,再将其思路利用到组件封装中去即可

高度组件封装代码思路:

依据show变量的标识,去更改dom.style.height;初始加载时,获取初始高度`dom
.offsetHeight更改一次、当show变量标识发生变化的时候,再更改一次。同时搭配高度的transition款式管制即可(即:监听propsshow`标识的变动更改之)

封装好的高度过渡组件代码如下:

<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>
另外饿了么UI也提供了el-collapse-transition组件,也是一个不错的抉择

对于组件中的role属性和aria-multiselectable

封装一套弱小的开源组件其实要思考的货色很多,比方须要适配屏幕阅读器,咱们看一下饿了么UI的el-collapse组件应用到的两个屏幕阅读器属性rolearia-multiselectable。如下图:

  • role属性是html中语义化标签的进一步补充(如 屏幕阅读器,给盲人应用),另举一个例子
  • <div role="checkbox" aria-checked="checked" /> 高度屏幕阅读器,此处有一个复选框,而且曾经被选中了
  • aria-multiselectable='true'告知辅助设施,一次能够开展多个项,还是只能开展一个

详情见css大神,张鑫旭的博客文章:https://www.zhangxinxu.com/wo...

由此能够看出,一套开源的组件,确实是方方面面都要思考到。

封装的组件

咱们先看一下效果图

封装的效果图

应用本人封装的折叠组件

<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组件

<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>
上述代码联合正文,更好的了解哦。当然完整版代码,在github仓库上,若可能帮到您一点点,欢送大手一挥,给个star哦 ^_^