造轮子-nav组件

2019-03-01  本文已影响0人  sweetBoy_9126

补充知识:
如果你需要相对引用你得加一个./否则会被认为你是在引用一个第三方库
比如:

//错误引入方法
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

<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)
    }
})
<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来说默认的不需要加名字

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

return {
        selected: ['girl']
 }

解决方法:使用依赖注入实现跨级调用

  1. 在你的根组件里提供一个依赖root,把当前的实例给root,然后定义一个addItem的方法,接受又来接收每个后代的实例
  2. 在你所有的需要作用的后代(item)中注入这个root
  3. 直接使用this.root.addItem这个函数把每个item的实例传给根组件
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])
                }
            })
        })

    }
}
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

data(){
  namePath: []
},
provide(){
  root: this
}
methods: {
    onClick(){
        this.root.namePath = []
        this.$parent.updateNamePath && this.$parent.updateNamePath()
        this.$emit('add:selected',this.name)
    }
}
<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

<div class="lf-sub-nav" :class="{active}" v-click-outside="close">
import ClickOutside from '../click-outside.js'
directives: {ClickOutside}

支持垂直导航

用户传入一个vertical来实现,然后在sub-nav中注入这个vertical

<lf-nav :selected.sync="selected" vertical>

</lf-nav>
<div class="lf-nav" :class="{vertical}">
    <slot></slot>
</div>
<script>
  props: {
    vertical: {
      type: Boolean
    }
  },
provide(){
    return {
        root: this,
        vertical: this.vertical
    }
},
</script>
<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>
上一篇 下一篇

猜你喜欢

热点阅读