美文网首页
造轮子-nav组件

造轮子-nav组件

作者: sweetBoy_9126 | 来源:发表于2019-03-01 13:00 被阅读0次

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

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

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

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

    相关文章

      网友评论

          本文标题:造轮子-nav组件

          本文链接:https://www.haomeiwen.com/subject/jwcduqtx.html