社区精选 | elementui源码学习之仿写一个el-collapse
今天小编为大家带来的是社区作者 水冗水孚 的文章,让我们一起来学习elementui源码。
组件思考
组件的需求
点击折叠头部区域展开或关闭折叠内容体区域 展开或折叠的时候,加上过渡效果 头部区域的内容文字参数定义 是否隐藏折叠的小箭头 手风琴模式的折叠面板(默认是都可以展开折叠的)
组件实现之父组件统一更改所有子组件状态
父组件传递数据,子组件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>
组件实现之高度过渡效果组件的封装
<!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>
.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>
关于组件中的role属性和aria-multiselectable等
role属性是html中语义化标签的进一步补充(如 屏幕阅读器,给盲人使用),另举一个例子 <div role="checkbox" aria-checked="checked" /> 高度屏幕阅读器,此处有一个复选框,而且已经被选中了 aria-multiselectable='true'告知辅助设备,一次可以展开多个项,还是只能展开一个
封装的组件
封装的效果图
使用自己封装的折叠组件
<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>
关注公众号:拾黑(shiheibook)了解更多
赞助链接:
关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/