共计 7336 个字符,预计需要花费 19 分钟才能阅读完成。
本篇文章记录仿写一个
el-collapse
组件细节,从而有助于大家更好了解饿了么 ui 对应组件具体工作细节。本文是 elementui 源码学习仿写系列的又一篇文章,后续闲暇了会不断更新并仿写其余组件。源码在 github 上,大家能够拉下来,npm start 运行跑起来,联合正文有助于更好的了解。github 仓库地址如下:https://github.com/shuirongsh…
组件思考
el-collapse
即为折叠面板的意思,个别次要是用于:对简单区域进行分组和暗藏,放弃页面的整洁,有分类整理的意思。
collapse
有折叠的意思,不过fold
也有折叠的意思。所以笔者这里封装的组件就改名字了,不叫my-collapse
,叫做my-fold
组件的需要
咱们先看一下下图折叠组件的结构图
联合上图曾经工作教训,大抵剖析组件的需要有以下:
- 点击折叠头部区域开展或敞开折叠内容体区域
- 开展或折叠的时候,加上过渡成果
- 头部区域的内容文字参数定义
- 是否暗藏折叠的小箭头
- 手风琴模式的折叠面板(默认是都能够开展折叠的)
组件实现之父组件对立更改所有子组件状态
个别状况下父组件更改子组件数据状态有以下形式:
- 父组件传递数据,子组件 props 接管。更改父组件数据,子组件也就主动更改更新了
- 应用
this.$refs.child.xxx = yyy
,给子组件打一个ref
,间接更改对应值即可 - 应用
this.$children
能够拜访所有的子组件实例对象。所以,也能够间接更改,如下:
父组件代码
// 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;});
},
其中一个子组件代码,另外两个也一样
// 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 款式管制即可(即:监听
props 中
show` 标识的变动更改之)
封装好的高度过渡组件代码如下:
<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
组件应用到的两个屏幕阅读器属性 role
和aria-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 哦
^_^