vue 自定义指令封装一个手风琴嵌套组件
2020-02-17 本文已影响0人
一颗数据小白菜
很多教程的手风琴组件都是一个v-for数组来实现手风琴组件,v-for封装起来很简单,但是我认为并不好。
理由如下:
- 这种方式很不优雅
- 其实我没用过这种实现方式的手风琴,不知道到底怎么实现配合router来实现展开和激活?感觉v-for这种实现手风琴的方式实现这个功能不太容易。
- 无法灵活嵌套
所以自己用自定义指令实现了一个手风琴组件。
代码很长,不想学习的可以直接github下载代码run serve直接使用。
-
先看效果:

- 支持初始化撑开多个折叠版,点击任意一个之后会将其他撑开的都关闭。
- 初始撑开为props 参数visible=true
- 后续会更新支持多个不同手风琴,支持开启手风琴模式
结构
-
边侧导航栏结构
-
<!-- 由于本身是封装的一个边侧导航栏,所有组件中有
NLY-accordionNav
NLY-accordionNavItem
NLY-accordionNavTree
-->
<NLY-accordionNav>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
Nejinn
</NLY-accordionNavItem>
<NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.sss>
Nejinn
</NLY-accordionNavTree>
<NLY-accordionNavCollapse id="sss" visible>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
lerity
</NLY-accordionNavItem>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
blog
</NLY-accordionNavItem>
</NLY-accordionNavCollapse>
</NLY-accordionNav>
-
只有手风琴折叠版的结构
-
<任意元素 v-nly-accordion.collapseId>
</任意元素元素>
<NLY-accordionNavCollapse id='collapseId'>
...嵌套元素,随意插入
</NLY-accordionNavCollapse>
demo:
<div v-nly-accordion.collapse1>点击我收起或展开 collapse1</div>
<NLY-accordionNavCollapse id='collapse1'>
<a>我是折叠版中的元素</a>
</NLY-accordionNavCollapse>
<div v-nly-accordion.collapse2>点击我收起或展开collapse2</div>
<NLY-accordionNavCollapse id='collapse2'>
<a>我是折叠版中的元素</a>
</NLY-accordionNavCollapse>
使用这种结构的时候,请注意自己写css。可以在accordion.vue中修改就行。
组件目录结构

自定义指令 v-nly-accordion
- nlyaccordion.js
// nlyaccordion.js
import Vue from "vue";
/**
* 差集函数
*/
function getDifference(allCollapseId, idKeys) {
let mixArray = [];
allCollapseId.forEach(item => {
if (idKeys.indexOf(item) == -1) {
mixArray.push(item);
}
});
return mixArray;
}
Vue.directive("nly-accordion", function(el, binding, vnode) {
/**
* 初始化指令时监控collapseStatus事件,collapseStatus事件由NLY-accordionNavCollapse组件发出,有2个参数,
* 一个是NLY-accordionNavCollapse事件props参数id,
* 一个是NLY-accordionNavCollapse折叠状态show
* function(a,b)中a是show,b是id
*/
vnode.context.$root.$on("collapseStatus", function(a, b) {
// 将所有提交collapseStatus事件的NLY-accordionNavCollapse组件的id放入allCollapseId
if (allCollapseId.indexOf(b) == -1) {
allCollapseId.push(b);
}
/**
* 获取当前指令的modifiers,如果当前指令的modifiers中包含提交collapseStatus事件的NLY-accordionNavCollapse组件的id
* 则初始化挂载指令的组件或者element的class,且修改当前指令modifiers为对应的show值
* 对应的初始化show为true,则在当前挂载指令的element的class中添加open
* 对应的初始化show为false,则在当前挂载指令的element的class中移除open
*/
let idKeys = Object.keys(binding.modifiers);
if (idKeys.indexOf(b) != -1) {
binding.modifiers[String(b)] = a;
if (a) {
el.classList.add("open");
} else {
el.classList.remove("open");
}
}
});
/**
* 新建一个array储存组件NLY-accordionNavCollapse的id
* 注意会先执行这里的代码再执行上面的代码。
*/
let allCollapseId = [];
/**
* 点击事件
*/
el.onclick = function() {
// 获取指令的modifiers
let idKeys = Object.keys(binding.modifiers);
// 求出modifiers和储存所有id的数组的差集
let mixArray = getDifference(allCollapseId, idKeys);
/**
* 循环当前指令的modifiers,并循环当前挂载指令实例的父组件的所有子组件
* 以组件的id找出指令对应的组件,执行对应的展开折叠动作
*/
idKeys.forEach(idKeysItem => {
vnode.componentInstance.$parent.$children.forEach(childrenItem => {
if (childrenItem.id == idKeysItem) {
if (binding.modifiers[idKeysItem]) {
childrenItem.show = false;
el.classList.remove("open");
} else {
childrenItem.show = true;
el.classList.add("open");
}
}
});
});
/**
* 判断当前指令的modifiers是否有对应的NLY-accordionNavCollapse组件
* 如果有就执行关闭其他NLY-accordionNavCollapse组件的动作,如果没有,就不进行操作
*/
idKeys.forEach(idKeysItem => {
if (allCollapseId.indexOf(idKeysItem) != -1) {
mixArray.forEach(mixArrayItem => {
vnode.componentInstance.$parent.$children.forEach(childrenItem => {
if (childrenItem.id == mixArrayItem) {
childrenItem.show = false;
el.classList.remove("open");
}
});
});
}
});
};
});
动画过渡组件
- 动画过渡组件是借鉴element-ui的,但是说实话,我不是很喜欢这个。
- collapse动画在NLY-accordionNavCollapse组件中引入
<script>
import collapse from "./collapse.js";
export default {
name: "AccordionNavCollapse",
components: {
collapse: collapse
},
- collapse.js
// collapse.js
const elTransition =
"0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out";
const Transition = {
"before-enter"(el) {
el.style.transition = elTransition;
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
},
enter(el) {
el.dataset.oldOverflow = el.style.overflow;
if (el.scrollHeight !== 0) {
el.style.height = el.scrollHeight + "px";
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
} else {
el.style.height = "";
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
el.style.overflow = "hidden";
},
"after-enter"(el) {
el.style.transition = "";
el.style.height = "";
el.style.overflow = el.dataset.oldOverflow;
},
"before-leave"(el) {
if (!el.dataset) el.dataset = {};
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) {
if (el.scrollHeight !== 0) {
el.style.transition = elTransition;
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
},
"after-leave"(el) {
el.style.transition = "";
el.style.height = "";
el.style.overflow = el.dataset.oldOverflow;
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
};
export default {
name: "collapseTransition",
functional: true,
render(h, { children }) {
const data = {
on: Transition
};
return h("transition", data, children);
}
};
组件
NLY-accordionNav
// AccordionNav.vue
<template>
<nav class="nly-blog-sider-nav flex-column">
<ul class="nly-blog-sider-menu flex-column">
<slot />
</ul>
</nav>
</template>
<script>
export default {
name: "AccordionNav"
};
</script>
NLY-accordionNavItem
// AccordionNavItem.vue
<template>
<li class="nly-blog-sider-menu-item">
<a class="nly-blog-sider-menu-title">
<i :class="iconClass" v-if="icon"> </i>
<p>
<slot />
</p>
</a>
</li>
</template>
<script>
export default {
name: "AccordionNavItem",
props: {
icon: {
type: String
}
},
computed: {
iconClass: function() {
return ["nly-blog-sider-menu-icon", this.icon];
}
}
};
</script>
NLY-accordionNavTree
// AccordionNavTree.vue
<template>
<li class="nly-blog-sider-menu-item">
<a class="nly-blog-sider-menu-title">
<i :class="iconClass" v-if="icon"> </i>
<p>
<slot />
</p>
<i class="nly-blog-sider-menu-arrow"> </i>
</a>
</li>
</template>
<script>
export default {
name: "AccordionNavTree",
props: {
icon: {
type: String
}
},
computed: {
iconClass: function() {
return ["nly-blog-sider-menu-icon", this.icon];
}
}
};
</script>
NLY-accordionNavCollapse
// AccordionNavCollapse.vue
<template>
<collapse>
<ul class="nly-blog-sider-menu menu-tree" v-show="show">
<slot />
</ul>
</collapse>
</template>
<script>
import collapse from "./collapse.js";
export default {
name: "AccordionNavCollapse",
components: {
collapse: collapse
},
data() {
return {
show: this.visible
};
},
model: {
prop: "visible",
event: "input"
},
props: {
visible: {
type: Boolean,
default: false
},
id: {
type: [String, Number]
}
},
created() {
this.show = this.visible;
this.$nextTick(function() {
this.emitState();
});
},
computed: {},
methods: {
emitState: function emitState() {
// 告诉指令当前id和show
this.$root.$emit("collapseStatus", this.show, this.id);
}
},
mounted() {},
watch: {
visible: function(newval, oldval) {
if (newval != oldval) {
this.show = newval;
}
},
show: function show(newVal, oldVal) {
if (newVal !== oldVal) {
this.emitState();
}
}
}
};
</script>
注册组件
- index.js
import AccordionNav from "./AccordionNav.vue";
import AccordionNavItem from "./AccordionNavItem.vue";
import AccordionNavTree from "./AccordionNavTree.vue";
import AccordionNavCollapse from "./AccordionNavCollapse.vue";
export default {
install: Vue => {
Vue.component("NLY-accordionNav", AccordionNav);
Vue.component("NLY-accordionNavItem", AccordionNavItem);
Vue.component("NLY-accordionNavTree", AccordionNavTree);
Vue.component("NLY-accordionNavCollapse", AccordionNavCollapse);
}
};
全局注册指令和组件
- main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
Vue.config.productionTip = false;
// 自定义图标,阿里巴巴矢量图标库
import "./assets/nlyblogfont/iconfont.css";
// 全局注册组件
import NLYblog from "./nlyaccordion";
Vue.use(NLYblog);
// 全局注册指令
import "./nlyaccordion/nlyaccordion.js";
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
Less
- 请把less放到app.vue或者手风琴的父组件中
- 也可以编译成css,然后再引入
<style lang="less">
.flex-column {
flex-direction: column !important;
}
.nly-blog-sider-nav {
padding: 0.5rem 1.5rem 0.5rem 1rem;
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
list-style: none;
.nly-blog-sider-menu {
margin-top: 1rem;
list-style: none;
&.menu-tree {
margin-top: 0;
margin-left: 1rem;
}
.nly-blog-sider-menu-item {
padding: 0 1rem 0 1rem;
&.open {
> .nly-blog-sider-menu-title {
i.nly-blog-sider-menu-arrow {
transform: translateY(-2px);
}
i.nly-blog-sider-menu-arrow::after {
transform: rotate(-45deg) translateX(-2px);
}
i.nly-blog-sider-menu-arrow::before {
transform: rotate(45deg) translateX(2px);
}
}
}
}
.nly-blog-sider-menu-title:hover {
color: #0fbcf9;
transition: color 0.3s ease-in;
.nly-blog-sider-menu-arrow::after {
background-color: #0fbcf9;
}
.nly-blog-sider-menu-arrow::before {
background-color: #0fbcf9;
}
}
.nly-blog-sider-menu-title {
// color: inherit;
white-space: nowrap;
cursor: pointer;
display: block;
color: #f97f51;
position: relative;
padding: 0.2rem 0.5rem 0.2rem 0.5rem;
transition: color 0.3s ease-in;
.nly-blog-sider-menu-icon {
// font-size: 1.8rem;
margin-right: 0.5rem;
color: inherit;
// vertical-align: -0.3rem;
}
p {
display: inline-block;
// font-size: 1.3rem;
color: inherit;
}
.nly-blog-sider-menu-arrow {
position: absolute;
top: 50%;
right: 16px;
width: 10px;
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.nly-blog-sider-menu-arrow::before {
transform: rotate(-45deg) translateX(2px);
position: absolute;
width: 6px;
height: 1.5px;
background-color: #f97f51;
border-radius: 2px;
transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
content: "";
}
.nly-blog-sider-menu-arrow::after {
transform: rotate(45deg) translateX(-2px);
position: absolute;
width: 6px;
height: 1.5px;
background-color: #f97f51;
border-radius: 2px;
transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
content: "";
}
}
}
}
</style>
Demo
<template>
<NLY-accordionNav>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
Nejinn
</NLY-accordionNavItem>
<NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.sss>
Nejinn
</NLY-accordionNavTree>
<NLY-accordionNavCollapse id="sss" visible>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
lerity
</NLY-accordionNavItem>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
blog
</NLY-accordionNavItem>
</NLY-accordionNavCollapse>
<NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.zzz>
一颗
</NLY-accordionNavTree>
<NLY-accordionNavCollapse id="zzz">
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
数据
</NLY-accordionNavItem>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
小白菜
</NLY-accordionNavItem>
</NLY-accordionNavCollapse>
<NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.ccc>
测试
</NLY-accordionNavTree>
<NLY-accordionNavCollapse id="ccc">
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
黄色
</NLY-accordionNavItem>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
蓝色
</NLY-accordionNavItem>
</NLY-accordionNavCollapse>
<NLY-accordionNavTree icon="nlyblog nly-blog-book" v-nly-accordion.ddd>
大巴
</NLY-accordionNavTree>
<NLY-accordionNavCollapse id="ddd">
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
上车
</NLY-accordionNavItem>
<NLY-accordionNavItem icon="nlyblog nly-blog-home">
不开车就下车
</NLY-accordionNavItem>
</NLY-accordionNavCollapse>
</NLY-accordionNav>
</template>
<script>
export default {
name: "accordion"
};
</script>
<style lang="less">
.flex-column {
flex-direction: column !important;
}
.nly-blog-sider-nav {
padding: 0.5rem 1.5rem 0.5rem 1rem;
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
list-style: none;
.nly-blog-sider-menu {
margin-top: 1rem;
list-style: none;
&.menu-tree {
margin-top: 0;
margin-left: 1rem;
}
.nly-blog-sider-menu-item {
padding: 0 1rem 0 1rem;
&.open {
> .nly-blog-sider-menu-title {
i.nly-blog-sider-menu-arrow {
transform: translateY(-2px);
}
i.nly-blog-sider-menu-arrow::after {
transform: rotate(-45deg) translateX(-2px);
}
i.nly-blog-sider-menu-arrow::before {
transform: rotate(45deg) translateX(2px);
}
}
}
}
.nly-blog-sider-menu-title:hover {
color: #0fbcf9;
transition: color 0.3s ease-in;
.nly-blog-sider-menu-arrow::after {
background-color: #0fbcf9;
}
.nly-blog-sider-menu-arrow::before {
background-color: #0fbcf9;
}
}
.nly-blog-sider-menu-title {
// color: inherit;
white-space: nowrap;
cursor: pointer;
display: block;
color: #f97f51;
position: relative;
padding: 0.2rem 0.5rem 0.2rem 0.5rem;
transition: color 0.3s ease-in;
.nly-blog-sider-menu-icon {
// font-size: 1.8rem;
margin-right: 0.5rem;
color: inherit;
// vertical-align: -0.3rem;
}
p {
display: inline-block;
// font-size: 1.3rem;
color: inherit;
}
.nly-blog-sider-menu-arrow {
position: absolute;
top: 50%;
right: 16px;
width: 10px;
transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.nly-blog-sider-menu-arrow::before {
transform: rotate(-45deg) translateX(2px);
position: absolute;
width: 6px;
height: 1.5px;
background-color: #f97f51;
border-radius: 2px;
transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
content: "";
}
.nly-blog-sider-menu-arrow::after {
transform: rotate(45deg) translateX(-2px);
position: absolute;
width: 6px;
height: 1.5px;
background-color: #f97f51;
border-radius: 2px;
transition: background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
top 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
content: "";
}
}
}
}
</style>
这时候运行就可以看到效果图的大手风琴折叠板。