vue实现一个简易Popover组件

概述

之前写vue的时候,对于下拉框,我是通过在组件内设置标记来控制是否弹出的,但是这样有一个问题,就是点击组件外部的时候,怎么也控制不了下拉框的关闭,用户体验非常差。

当时想到的解决方法是:给根实例创建一个标记来控制,然后一级一级的把这个标记传进来。但是这样每次配置都要改根组件,非常不灵活

最近看museUI库,发现它的下拉框Select实现的非常灵活,点击组件外也能控制下拉框关闭,于是想探究一番,借此机会也深入学习一下vue。

museUI源码

首先去看Select的源码:

directives: [{
    name: 'click-outside',
    value: (e) => {
        if (this.open && this.$refs.popover.$el.contains(e.target)) return;
        this.blur();
    }
 }],

可以看到,有个click-outsidepopover,然后它是通过用自定义指令directives实现的。然后去museUI搜popover,果然这是一个弹出组件,并且能够在组件外部控制弹窗关闭。于是开始看popover的源码:

close (reason) {
    if (!this.open) return;
    this.$emit('update:open', false);
    this.$emit('close', reason);
},
clickOutSide (e) {
    if (this.trigger && this.trigger.contains(e.target)) return;
    this.close('clickOutSide');
},

可以看到,它也是通过click-outside来实现的,click-outside字面意思是点击外面,应该就是这个了。然后看click-outside的源码

name: 'click-outside',
bind (el, binding, vnode) {
  const documentHandler = function (e) {
    if (!vnode.context || el.contains(e.target)) return;
    if (binding.expression) {
      vnode.context[el[clickoutsideContext].methodName](e);
    } else {
      el[clickoutsideContext].bindingFn(e);
    }
  };
  el[clickoutsideContext] = {
    documentHandler,
    methodName: binding.expression,
    bindingFn: binding.value
  };
  setTimeout(() => {
    document.addEventListener('click', documentHandler);
  }, 0);
},

原来它是通过自定义指令,在组件创建的时候,给document绑定一个全局click事件,当点击document的时候,通过判断点击节点来控制弹窗关闭的。这差不多就是事件代理

所以总结一下,要实现组件外部控制组件弹窗的关闭,主要利用directives,bind,document就行了。

自己实现

既然知道原理就有点跃跃欲试了,通过查阅官方文档得知,directives可以用于局部组件,这样就变成了局部指令。于是写代码如下:

<template>
    <div class="pop-over">
        <a @click="toggleOpen" class="pop-button" href="javascript: void(0);">
            {{ 按钮1 }}
        </a>
        <ul v-clickoutside="close" v-show="open" class="pop-list">
            <li>选项1</li>
            <li>选项2</li>
            <li>选项3</li>
            <li>选项4</li>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'PopOver',
    data() {
        return {
            open: false
        }
    },
    methods: {
        toggleOpen: function() {
            this.open = !this.open;
        },
        close: function(e) {
            if(this.$el.contains(e.target)) return;
            this.open = false;
        }
    },
    directives: {
        clickoutside: {
            bind: function (el, binding, vnode) {
                const documentHandler = function (e) {
                    if (!vnode.context || el.contains(e.target)) return;
                    binding.value(e);
                };

                setTimeout(() => {
                    document.addEventListener('click', documentHandler);
                }, 0);
            }
        }
    }
}
</script>

注意,在我们close方法里面,我们通过判断点击节点是否被组件包含,如果包含的话,不执行关闭行为。

但是上面的组件不通用,正好官方文档学习了slot,于是用slot改写如下:

<template>
    <div class="pop-over">
        <a @click="toggleOpen" class="pop-button" href="javascript: void(0);">
            {{ buttonText }}
        </a>
        <ul v-clickoutside="close" v-show="open" class="pop-list">
            <slot></slot>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'PopOver',
    props: ['buttonText'],
    data() {
        return {
            open: false
        }
    },
    methods: {
        toggleOpen: function() {
            this.open = !this.open;
        },
        close: function(e) {
            if(this.$el.contains(e.target)) return;
            this.open = false;
        }
    },
    directives: {
        clickoutside: {
            bind: function (el, binding, vnode) {
                const documentHandler = function (e) {
                    if (!vnode.context || el.contains(e.target)) return;
                    binding.value(e);
                };

                setTimeout(() => {
                    document.addEventListener('click', documentHandler);
                }, 0);
            }
        }
    }
}
</script>

<style scoped>
.pop-over {
    position: relative;
    width: 100%;
    height: 100%;
}
.pop-button {
    position: relative;
    width: 100%;
    height: 100%;
    text-decoration:none;
    color: inherit;
}
.pop-list {
    position: absolute;
    left: 0;
    top: 0;
}
.pop-list li {
    width: 100%;
    height: 100%;
    padding: 8px 3px;
    list-style:none;
}
</style>

利用props自定义按钮文字,slot自定义弹窗文字,这样一个简易的Popover组件就完成了。

我学到了什么

  1. directives自定义指定,事件代理,slot练手一番,感觉很爽。
  2. 在看源码的过程中,也看到了render方法的使用,以及museUI的组件化思想
  3. 对于组件外控制组件的行为有了新的思路。

原文地址:https://www.cnblogs.com/yangzhou33/p/10023410.html

时间: 2024-10-19 00:18:44

vue实现一个简易Popover组件的相关文章

Vue实现一个星级评分组件

星级评分在WEB开发中经常可能会用到,因此把他封装成一个vue组件是很合适的,要做的效果如下: 因此我们需要通过后台传递过来的score来写业务逻辑 比如这个模拟数据,3.6分就应该是三颗半星,小于3.5分大于3分的依旧按3分判断 在设计给出的图标中单颗星级有无星级,半颗,以及一颗 根据星星的大小,还会给出24,36,48格式大小的星星,因此这个组件的逻辑还是比较复杂的 star组件需要两个从外部传递进来的参数,一个是star的大小,一个是分数 因此在props里定义这两个从外部传递进来的变量

Angular2-编写一个简易的组件

Angular2组件可以这么理解:编写一个类,然后在类的上面用组件装饰器装饰一下,这个类就成组件了. 所以编写组件分两步:1)编写类:2)编写装饰器 1)编写类: export class SimpleComponent {} 2)在类上面编写装饰器: @Component({ selector: 'simple-component', template: `Hello Simple Component!` }) 完整代码如下: @Component({ selector: 'simple-co

vue小案例--简易评论区

一.小案例(评论区) 1.流程 (1)分析静态页面.(vue项目创建参考https://www.cnblogs.com/l-y-h/p/11241503.html)(2)拆分静态页面,变成一个个组件.(3)对组件编码,生成动态页面. 2.静态页面 参考来源:https://www.bilibili.com/video/av49099807/?p=22&t=1223 [举例:] <!DOCTYPE html> <html> <head> <meta char

vue + socket.io实现一个简易聊天室

vue + vuex + elementUi + socket.io实现一个简易的在线聊天室,提高自己在对vue系列在项目中应用的深度.因为学会一个库或者框架容易,但要结合项目使用一个库或框架就不是那么容易了.功能虽然不多,但还是有收获.设计和实现思路较为拙劣,恳请各位大大指正. 可以达到的需求 能查看在线用户列表 能发送和接受消息 使用到的框架和库 socket.io做为实时通讯基础 vuex/vue:客户端Ui层使用 Element-ui:客户端Ui组件 类文件关系图 服务端: 客户端: 服

Vue学习笔记入门篇——组件的使用

本文为转载,原文:Vue学习笔记入门篇--组件的使用 组件定义 组件 (Component) 是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能.在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展. 组件使用 注册 注册一个全局组件,你可以使用 Vue.component(tagName, options).组件在注册之后,便可以在父实例的模块中以自定义元素 的形式使用.

Vue学习笔记入门篇——组件的内容分发(slot)

本文为转载,原文:Vue学习笔记入门篇--组件的内容分发(slot) 介绍 为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板.这个过程被称为 内容分发 (或 "transclusion" 如果你熟悉 Angular).Vue.js 实现了一个内容分发 API,使用特殊的 'slot' 元素作为原始内容的插槽. 编译作用域 在深入内容分发 API 之前,我们先明确内容在哪个作用域里编译.假定模板为: <child-component> {{ messa

Angularjs,WebAPI 搭建一个简易权限管理系统

Angularjs,WebAPI 搭建一个简易权限管理系统 Angularjs名词与概念(一) 1. 目录 前言 Angularjs名词与概念 权限系统原型 权限系统业务 数据库设计和实现 WebAPI项目主体结构 Angularjs前端主体结构 2. 前言 Angularjs开发CRUD类型的Web系统生产力惊人,与jQuery,YUI,kissy,Extjs等前端框架区别非常大,初学者在学习的过程中容易以自己以往的经验来学习Angularjs 往往走入误区,最典型的特征是在的开发过程中,使用

Vue学习笔记入门篇——组件的通讯

本文为转载,原文:Vue学习笔记入门篇--组件的通讯 组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B.它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件.然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的.这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性.在 Vue 中,父子组件的关系可以总结为 props down, events up.父组件通过 props 向下传递

基于Vue.js的表格分页组件

BootPage组件简介 其实也不是啥高大上的组件了,相反确实一个简单的表格分页组件而已,主要是自己最近项目中需要一个表格分页组件,而Vue官方组件库里分页组件都功能太强大或者没有适合我的,所以就自己写了一个凑合着用,或许有人和我一样需要这样一个简单的分页组件来实现简单的分页功能,我便在这里分享一下,大家自觉填坑咯. 如需高大上的组件,可以移步Vue官方组件库:https://github.com/vuejs/awesome-vue#libraries--plugins BootPage是一款支