补充知识:
如果你需要相对引用你得加一个./否则会被认为你是在引用一个第三方库
比如:
//错误引入方法
import LfNav from 'nav/nav.vue'
//正确引入方法
import LfNav from './nav/nav.vue'
首先我们需要三个组件分别是nav.vue/nav-item.vue/sub-nav.vue
最开始的结构(无子菜单的结构)
<lf-nav :selected.sync="selected">
<lf-nav-item name="home">首页</lf-nav-item>
<lf-nav-item name="about">关于</lf-nav-item>
<lf-nav-item name="hire">招聘</lf-nav-item>
</lf-nav>
如何实现在父组件中slot里的子组件触发事件然后对应的在父组件中监听
思路:slot中的子组件先触发一个事件,然后在父组件中通过this.$children
拿到slot
里的子组件,之后遍历出每一个vm,让每一个vm都监听这个事件也就是vm.$on
- nav.vue
<template>
<div class="lf-nav">
<slot></slot>
</div>
</template>
vm.$on('add:selected',(name)=>{
console.log(name)
if(this.selected.indexOf(name) > -1 ){
}else{
let copy = JSON.parse(JSON.stringify(this.selected))
copy.push(name)
console.log(copy)
this.$emit('update:selected',copy)
}
})
- nav-item.vue
<template>
<div class="lf-nav-item" :class="{active: selected}" @click="onClick">
<slot></slot>
</div>
</template>
<script>
export default {
name: "LiFaNavItem",
props: {
name: {
type: String,
required: true
}
},
data(){
return {
selected: undefined
}
},
methods: {
onClick(){
console.log(this.name)
this.$emit('add:selected',this.name)
}
}
}
</script>
添加子菜单
对于slot来说默认的不需要加名字
- demo.vue
<template>
<div>
<lf-nav :selected.sync="selected">
<lf-nav-item name="home">首页</lf-nav-item>
<lf-sub-nav name="about">
<template slot="title">关于</template>
<lf-nav-item name="girl">美女</lf-nav-item>
<lf-nav-item name="boy">帅哥</lf-nav-item>
<lf-nav-item name="old">老爷爷</lf-nav-item>
</lf-sub-nav>
<lf-nav-item name="hire">招聘</lf-nav-item>
</lf-nav>
</div>
</template>
- sub-nav.vue
<template>
<div class="lf-sub-nav">
<span>
//这个slot对应的上面的title
<slot name="title"></slot>
</span>
<div class="popover">
//这个是默认的不需要加名字,对应的就是上面的<lf-nav-item>
<slot></slot>
</div>
</div>
</template>
遇到的问题:由于我们中间多了一层sub-nav,导致我们不能直接通知nav-item,而需要通知su-nav,让它去通知nav-item
- demo.vue
return {
selected: ['girl']
}
解决方法:使用依赖注入实现跨级调用
- 在你的根组件里提供一个依赖root,把当前的实例给root,然后定义一个addItem的方法,接受又来接收每个后代的实例
- 在你所有的需要作用的后代(item)中注入这个root
- 直接使用this.root.addItem这个函数把每个item的实例传给根组件
- nav.vue
data(){
return {
item: []
}
},
provide(){
return {
root: this
}
},
mounted() {
this.updateChildren()
this.listenToChildren()
},
updated() {
this.updateChildren()
this.listenToChildren()
},
methods: {
addItem(vm){
this.item.push(vm)
},
updateChildren: function () {
this.item.forEach(vm => {
if (this.selected.indexOf(vm.name) > -1) {
vm.selected = true
} else {
vm.selected = false
}
})
},
listenToChildren(){
this.item.forEach(vm=>{
vm.$on('add:selected', (name) => {
if(this.multiple){
if (this.selected.indexOf(name) <= -1) {
let copy = JSON.parse(JSON.stringify(this.selected))
copy.push(name)
this.$emit('update:selected', copy)
}
}else{
this.$emit('update:selected',[name])
}
})
})
}
}
- nav-item.vue
props: {
name: {
type: String,
required: true
}
},
data(){
return {
selected: undefined
}
},
created(){
this.root.addItem(this)
},
methods: {
onClick(){
this.$emit('add:selected',this.name)
}
}
}
使用v-if遇到一个bug
我们给子菜单sub-nav组件添加一个v-if默认是false,当点击title的时候为true,但是我们发现点击子菜单里的item不能添加active的类,原因是我们sub-nav里的item实例是在created的时候添加到nav中的,而v-if为false的时候item组件并不执行created钩子,所以不会添加active
解决办法:将v-if换成v-show
实现多级菜单
bug:当我们点击关于下面的菜单的时候,他只能选中当前级下的一个,没法通知到关于被选中了
比如我们上面的我们实际上是希望关于下面的联系方式>手机>移动都被选中,这样我们再次点击关于的时候能通知到它下面这些层级都被选中
实现方法:首先通过nav.vue
里的data
中声明一个namePath
为空数组,然后在nav-item.vue
中注入这个nav的实例root
,点击的时候让namePath
为空数组,然后调用它的父组件里的updateNamePath
方法,在sub-nav
中声明这个方法,因为不止一层,有可能sub-nav.vue
还有父组件所以需要在 sub-nav.vue
中也调用updateNamePath
然后把当前的name
传给root里的namePath
通过在sub-nav中判断root.namePath
是否包含this.name
来添加active
类
- nav.vue
data(){
namePath: []
},
provide(){
root: this
}
- nav-item.vue
methods: {
onClick(){
this.root.namePath = []
this.$parent.updateNamePath && this.$parent.updateNamePath()
this.$emit('add:selected',this.name)
}
}
- sub-nav.vue
<div class="lf-sub-nav" :class="{active}">
</div>
<script>
inject: ['root'],
props: {
name: {
type: String,
required: true
}
},
computed: {
active(){
return this.root.namePath.indexOf(this.name) >= 0 ? true : false
}
},
methods: {
updateNamePath(){
this.$parent.updateNamePath && this.$parent.updateNamePath()
this.root.namePath.push(this.name)
}
}
</script>
改进:当点击sub-nav的外面的时候隐藏二级菜单
引入之前自定义的指令click-outside
- sub-nav.vue
<div class="lf-sub-nav" :class="{active}" v-click-outside="close">
import ClickOutside from '../click-outside.js'
directives: {ClickOutside}
支持垂直导航
用户传入一个vertical来实现,然后在sub-nav中注入这个vertical
- demo.vue
<lf-nav :selected.sync="selected" vertical>
</lf-nav>
- nav.vue
<div class="lf-nav" :class="{vertical}">
<slot></slot>
</div>
<script>
props: {
vertical: {
type: Boolean
}
},
provide(){
return {
root: this,
vertical: this.vertical
}
},
</script>
- sub-nav.vue
<transition @enter="enter" @leave="leave"
@after-enter="afterEnter" @after-leave="afterLeave"
>
<div class="popover" v-show="open" :class="{vertical}">
<slot></slot>
</div>
</transition>
<script>
inject: ['vertical'],
methods: {
enter(el, done){
//先设置为auto来获取它的高度
el.style.height = 'auto'
let {height} = el.getBoundingClientRect()//113
//然后让他等于0,因为高度的变化只能是数字之间0-113而不能是auto-113
el.style.height = 0
//之所以要加el.getBoundingClientRect(),是因为如果不加浏览器会对你的
//多次赋值进行合并,也就是说你先赋值了0,接着赋值113,它只会记下你的最后这一次113
//而如果你想让0也生效,就需要在它赋值后紧接着进行一个与高度有关的操作
el.getBoundingClientRect()
//最后让高度等于你元素自身的高度
el.style.height = `${height}px`
el.addEventListener('transitionend',()=>{
done()
})
},
leave(el,done){
let {height} = el.getBoundingClientRect()
el.style.height = `${height}px`
el.getBoundingClientRect()
el.style.height = 0
//这里之所以要监听transitionend是因为如果直接写done的话它就会直接
//display:none
el.addEventListener('transitionend',()=>{
done()
})
},
afterEnter(el){
el.style.height='auto'
},
afterLeave(el){
el.style.height = 'auto'
}
}
</script>
让横竖动画分开
<template v-if="vertical">
<transition @enter="enter" @leave="leave"
@after-enter="afterEnter" @after-leave="afterLeave"
>
<div class="popover" v-show="open" :class="{vertical}">
<slot></slot>
</div>
</transition>
</template>
<template v-else>
<div class="popover" v-show="open">
<slot></slot>
</div>
</template>
网友评论