7.1 简单轮子:VuePopover组件
大纲链接 §
[toc]
popover组件 需求
- 可以激活
- hover
- click
- 激活后弹出提示
- 简单文本提示
- 复杂内容
- 两种形式
- 按钮在popover组件中
- 封装为指令,在按钮中使用
API设计
popover组件 UI
为什么需要单独的 popover 组件
|
|
- 需要让浮动消息出现在按钮正上方
- 提供给用户以封装好样式的popover 组件,不用另外写样式
基本结构
|
|
先实现VuePopover组件包裹按钮的方式
显示和隐藏提示消息
使用展示组件
Popovers.vue
|
|
VuePopover.vue
|
|
- 在
slot上添加样式时无效的 - 需要
slot在外边再包裹一层div - 将
v-if="visible"的控制逻辑页移至外层div 如此就可以加样式,使得弹出消息框的
div可以定位1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39<template> <div class="popover" @click="togglePop"> <div class="content-wrapper" v-if="visible"> <slot name="content"></slot> </div> <slot></slot> </div> </template> <script lang="ts"> import {Component, Vue} from 'vue-property-decorator'; @Component export default class VuePopover extends Vue { name = 'VuePopover'; visible = false; togglePop() { this.visible = !this.visible; } } </script> <style lang="scss" scoped> .popover { display: inline-block; vertical-align: top; position: relative; .content-wrapper { position: absolute; bottom: 100%; left: 0; box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); padding: 2px; } } </style>
目前存在的 bug
- 未实现点击外边空白处,隐藏消息框
实现隐藏消息框
在
document.body上添加监听click事件
|
|
document.body.addEventListener('click', () => { this.visible = false; });- 当点击外部,就隐藏消框
无法出现消息框 bug
- 由于事件冒泡机制
- 未在点击事件结束后就添加监听点击事件
- 会连续触发点击切换与隐藏消息框两个执行逻辑因此导致了 bug
加两句log来测试
|
|
- 连续触发 点击切换 与 隐藏消息框
- 先把
this.visible变成true,紧接着就把this.visible变成false - 弹出后立马关闭
- 显然不符合预期的需求
使用异步this.$nextTick来控制执行顺序
|
|
另一个 bug
|
|
- 将
body区域使用border标识出来 - 当body未占满时,当点击下方非body区时,无法关闭消息框
解决方法时不监听
document.body,而直接监听document1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21import {Component, Vue} from 'vue-property-decorator'; @Component export default class VuePopover extends Vue { name = 'VuePopover'; visible = false; togglePop() { this.visible = !this.visible; console.log('切换 visible'); if (this.visible === true) { this.$nextTick(() => { document.addEventListener('click', () => { this.visible = false; console.log('点击body就关闭popover'); }); }); } } }
点三次点击的bug
- 点击按钮,出现消息框
- 点击外部空白,隐藏消息框
- 再次点击按钮,消息框未出现
原因
- 目前存在两个监听器,都在监听
click事件- 第一个是按钮在监听用户点击
- 第二个是
document上在监听点击 - 调用顺序是,先调用按钮的,在调用
document的
- 而
document上的监听事件未被移除掉,就在再次点击后添加了另一个点击事件监听 - 造成监听器越积越多
需要及时地销毁监听器
- 在
this.visible = false;后就销毁监听器 - 需要一个具名函数,并且注意
this的指向
尝试使用function x() {}.bind(this)
|
|
() => {}相当于function x() {}.bind(this)- 不改变
this的指向 - 尝试失败
.bind每次执行返回一个新的函数(堆内存中) - 事件添加的回调函数和(堆内存中) 删除的不是同一个函数
老老实实地声明
const eventHandler = () => {...}
|
|
还有几个 bug
改变节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40<template> <div class="popover" @click.stop="togglePop"> <div class="content-wrapper" v-if="visible"> <slot name="content"></slot> </div> <div class="trigger"> <slot></slot> </div> </div> </template> <script lang="ts"> import {Component, Vue} from 'vue-property-decorator'; @Component export default class VuePopover extends Vue { name = 'VuePopover'; visible = false; togglePop() { this.visible = !this.visible; console.log('切换 visible'); if (this.visible) { this.$nextTick(() => { console.log('声明回调函数'); const eventHandler = () => { this.visible = false; console.log('document 隐藏 popover'); console.log('删除监听器'); document.removeEventListener('click', eventHandler); }; console.log('添加监听函数'); document.addEventListener('click', eventHandler); }); } else {console.log('vm 隐藏 popover');} } } </script>点击多次按钮切换时,再点击
document,会累积执行多个document.removeEventListener('click', eventHandler);当出现弹出框时,点击弹出框,此时弹出框不应该消失
第一次点击显示popover,第二次点击按钮隐藏了两次popover
需要阻止冒泡
使用
.stop事件修饰符<div class="popover" @click.stop="togglePop">- 阻止popover组件上的点击事件冒泡到外层节点上
- 同时阻止消息框
content-wrapper节点上的点击事件冒泡到外层节点上 <div class="content-wrapper" v-if="visible" @click.stop>...1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32... togglePop() { this.visible = !this.visible; const eventHandler = () => { this.visible = false; console.log('document 隐藏 popover'); console.log('删除监听器'); document.removeEventListener('click', eventHandler); }; console.log('声明回调函数'); console.log('切换 visible'); if (this.visible) { this.$nextTick(() => { console.log('添加监听函数'); document.addEventListener('click', eventHandler); }); } else { console.log('vm 隐藏 popover'); document.removeEventListener('click', eventHandler); } console.log('--------------'); } ...
如果组件在一个有着
overflow: hidden;的样式的节点中
- 弹出消息框会被外层节点遮挡
- 应该将
popover组件的弹出消息框的节点放在</body>关闭标签前
@click.stop阻止冒泡带来的 bug
- 用户使用
popover组件时,无法使用监听组件外层点击事件 - 打断了用户的事件传播链
- 推翻使用
@click.stop
解决popover的三个问题
overflow: hidden;- 重复切换按钮多次,点击一次document,关闭重复n次
- 未取消监听 document
使用
ref属性,获取到 消息框的节点,并打印在控制台
|
|
- 注意,当使用了条件渲染
v-if时,未出现在文档流中的节点上的ref无法获取 - 补充
v-showV.S.v-if的知识点v-show只改变节点的样式v-if会改变节点存在于DOM树与否
获取
this.$refs.contentWrapper之后,就可以将获取到的节点添加到document.body上
|
|
- 当改变 vue 中组件节点在文档中的位置时,并不影响组件的功能
- 包括点击事件执行回调依然有效
- 只是需要注意的是,
- 如果vue文件的
<style lang="scss" scoped>...标签中如果写了scoped属性,并且类样式互相嵌套 - 则被改变位置的元素不再具有该样式
- 如果vue文件的
解决方法是将该类样式提出到嵌套最外边
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16... <style lang="scss" scoped> .popover { display: inline-block; vertical-align: top; position: relative; } .content-wrapper { display: block; position: absolute; box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); padding: 2px; transform: translateY(-100%); } </style>
不在组件挂载一完成时,就将消息框添节点加到
document.body上
|
|
- 而是等到当满足条件
this.visible === true显示时,再把消息框添节点加到document.body上 - 注意刚
this.visible === true时,节点并未出现在文档中,需要使用this.$nextTick - 这样就不用担心被外层
css样式设置的overflow: hidden;遮住 - 接下来只需获取按钮元素的位置,据此改变定位样式,让节点出现在按钮上方
用JS获取按钮元素的位置
- 同样地在
slot上方加ref引用属性是无效的 需要再包裹一层节点,在外层节点上添加
ref引用1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36<template> <div class="popover" @click.stop="togglePop"> <div ref="contentWrapper" class="content-wrapper" v-if="visible" @click.stop> <slot name="content"></slot> </div> <span class="trigger" ref="trigger"> <slot></slot> </span> </div> </template> <script lang="ts"> import {Component, Vue} from 'vue-property-decorator'; @Component export default class VuePopover extends Vue { name = 'VuePopover'; visible = false; togglePop() { this.visible = !this.visible; if (this.visible) { this.$nextTick(() => { document.body.appendChild(this.$refs.contentWrapper as Node); }); } } mounted() { console.log(this.$refs.trigger); } } </script>
使用Element.getBoundingClientRect() API获取元素尺寸与位置
|
|
- 将得到的值赋给
(this.$refs.contentWrapper as any).style.left(this.$refs.contentWrapper as any).style.top
- 并且调整一下样式
transform: translateY(100%);
解决容器有overflow hidden的bug
测试poopover
|
|
VuePopover.vue
|
|
- 注意相对的位置距离是不同的
clientLeft是相对于屏幕可视区域- 而绝对定位是相对于
body元素的左上坐标点的
- 需要得到相对于可视区域的差值
document.documentElement.scrollHeight获取页面的可滚动总高window.scrollY获取页面顶部至可视区域(视口)的顶部的高度,即文档在垂直方向已滚动的像素差值
- 补上差值
- (this.$refs.contentWrapper as any).style.top = `${top + window.scrollY}px`;
- 注意
scrollY的兼容性 - 同理
scrollX,出现X轴滚动条,相对位置发生变化 - 需要补上一个差值
- (this.$refs.contentWrapper as any).style.left = `${left + window.scrollX}px`;
搜索
js get element offset relative to body获取元素相对于body的偏移量(即偏移位置,包括top和left)
当用户点击按钮时,触发外层div的click事件的bug
会触发多余的关闭
- 一次是由
document引起的:document.removeEventListener('click', eventHandler); - 一次是 button 事件引起的
vue@2.5*和vue@2.6*的$nextTick不同
判断点击事件目标对象
目标对象是按钮,还是弹出消息框
|
|
- 实现点击按钮切换,点击消息框不切换的逻辑分开
- 判断点击的目标对象
event.target- 是否包含在
this.$refs.triggerWrapper中 - 即是否点击了按钮
this.$refs.triggerWrapper.contains(event.target)
- 是否包含在
- 当 弹出框 显示
- 将弹出框节点放到 body 子节点的最后
- 改变弹出框样式,使其出现在相对按钮合适的位置
当 popover 显示时 执行的逻辑
|
|
- 点击按钮 执行的方法:切换显示/隐藏 popover
- 点击按钮部分 执行的逻辑
- 切换显示/隐藏 popover
this.visible = !this.visible; - 当 popover 显示时 执行的逻辑, 必须为异步
- 显示 弹出框,将弹出框节点放到 body 子节点的最后
document.body.appendChild(this.$refs.contentWrapper as Node); - 改变弹出框样式,使其出现在相对按钮合适的位置
- 获取 按钮元素 左上顶点的位置坐标 top, left
- 设置 弹出消息框节点 的行内样式,使其定位到 按钮元素 上方
- 定义一个点击文档事件的回调函数
eventHandler - 如果点击了
document就隐藏 popover弹出框this.visible = false; - 移除 事件监听
document.removeEventListener('click', eventHandler); - 在文档上 添加 点击事件 的监听,必须为异步
this.requestAnimationFrameId = requestAnimationFrame(() => {...});document.addEventListener('click', eventHandler);
- 显示 弹出框,将弹出框节点放到 body 子节点的最后
- 当 popover 隐藏时 执行的逻辑
- 点击popover部分 执行的逻辑
重构 抽象提出
positionPoplistenToDocument方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109<template> <div class="popover" @click="togglePop"> <div ref="contentWrapper" class="content-wrapper" v-if="visible"> <slot name="content"></slot> </div> <span class="triggerWrapper" ref="triggerWrapper"> <slot>button</slot> </span> </div> </template> <script lang="ts"> import {Component, Vue} from 'vue-property-decorator'; @Component export default class VuePopover extends Vue { name = 'VuePopover'; visible = false; id = 0; positionPop() { // 获取 弹出消息框节点 的引用,放到 body 子节点的最后 console.log('this.$refs.contentWrapper: ', this.$refs.contentWrapper); document.body.appendChild(this.$refs.contentWrapper as Node); // 获取 按钮元素 左上顶点的位置坐标 top, left const {top, left} = (this.$refs.triggerWrapper as Element) .getBoundingClientRect(); // 设置 弹出消息框节点 的行内样式,使其定位到 按钮元素 上方 (this.$refs.contentWrapper as any).style.top = `${top + window.scrollY}px`; (this.$refs.contentWrapper as any).style.left = `${left + window.scrollX}px`; } listenToDocument() { // 定义一个点击事件的回调函数 const closeHandler = (e: Event) => { // 如果点击的目标对象 不存在于 包裹弹出框的div中 if (!((this.$refs.contentWrapper as Element)?.contains(e.target as Node))) { this.visible = false; console.log(this.visible, '立即关闭了弹出框'); document.removeEventListener('click', closeHandler); console.log('移除事件监听'); } }; // 在文档上 添加 点击事件 的监听 console.log('在文档上 添加 点击事件 的监听'); document.addEventListener('click', closeHandler); } // 点击按钮 执行的方法:切换显示/隐藏 popover togglePop(event: Event) { // 点击按钮部分 执行的逻辑 if ((this.$refs.triggerWrapper as HTMLElement) ?.contains(event.target as Node)) { // 切换显示/隐藏 popover this.visible = !this.visible; console.log('切换显示/隐藏 popover', this.visible); // 当 popover 显示时 执行的逻辑 // 显示 弹出框,将弹出框节点放到 body 子节点的最后 // 改变弹出框样式,使其出现在相对按钮合适的位置 if (this.visible) { this.$nextTick(() => { this.positionPop(); this.listenToDocument(); }); } } else { // 点击popover部分 执行的逻辑 console.log('点击popover部分 执行的逻辑'); } } } </script> <style lang="scss" scoped> .popover { display: inline-block; vertical-align: top; position: relative; .content-wrapper { background-color: white; } .triggerWrapper { } } .content-wrapper { display: block; position: absolute; box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); padding: 2px; transform: translateY(-100%); } </style>
在不阻止冒泡的条件下,判断事件的回调函数的目标对象
- 重构 抽象提出
closeHandlercloseEvent方法 - 如果目标对象 存在于 包裹弹出框的div中,则什么也不做
- 如果目标对象 不存在于 包裹弹出框的div中,则
- 隐藏 popover弹出框
this.visible = false; - 移除 事件监听
document.removeEventListener('click', eventHandler);
- 隐藏 popover弹出框
this.listenToDocument()还是会在点击事件冒泡完成之前执行必须使用
setTimeout来延迟,使 添加监听在 点击事件冒泡 之后 执行1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117<template> <div class="popover" @click="togglePop"> <div ref="contentWrapper" class="content-wrapper" v-if="visible"> <slot name="content"></slot> </div> <span class="triggerWrapper" ref="triggerWrapper"> <slot>button</slot> </span> </div> </template> <script lang="ts"> import {Component, Vue, Watch} from 'vue-property-decorator'; @Component export default class VuePopover extends Vue { name = 'VuePopover'; visible = false; positionPop() { // 获取 弹出消息框节点 的引用,放到 body 子节点的最后 console.log('this.$refs.contentWrapper: ', this.$refs.contentWrapper); document.body.appendChild(this.$refs.contentWrapper as Node); // 获取 按钮元素 左上顶点的位置坐标 top, left const {top, left} = (this.$refs.triggerWrapper as Element) .getBoundingClientRect(); // 设置 弹出消息框节点 的行内样式,使其定位到 按钮元素 上方 (this.$refs.contentWrapper as any).style.top = `${top + window.scrollY}px`; (this.$refs.contentWrapper as any).style.left = `${left + window.scrollX}px`; } closeEvent() { // 关闭 弹出框 this.visible = false; console.log(this.visible, '立即关闭了弹出框'); console.log('移除事件监听'); document.removeEventListener('click', this.closeHandler); } // 定义一个点击事件的回调函数 closeHandler(e: Event) { const hasPopover = ((this.$refs.contentWrapper as Element) ?.contains(e.target as Node)); // 如果 点击的目标对象 不存在于 包裹弹出框的div中 if (!hasPopover) { this.closeEvent(); } } listenToDocument() { // 在文档上 添加 点击事件 的监听 console.log('在文档上 添加 点击事件 的监听'); document.addEventListener('click', this.closeHandler); } // 点击按钮 执行的方法:切换显示/隐藏 popover togglePop(event: Event) { // 点击按钮部分 执行的逻辑 if ((this.$refs.triggerWrapper as HTMLElement) ?.contains(event.target as Node)) { // 切换显示/隐藏 popover this.visible = !this.visible; console.log('切换显示/隐藏 popover', this.visible); // 当 popover 显示时 执行的逻辑 // 显示 弹出框,将弹出框节点放到 body 子节点的最后 // 改变弹出框样式,使其出现在相对按钮合适的位置 /* if (this.visible) { this.$nextTick(() => { // 将 弹出框 放到body 里 this.positionPop(); }); }*/ } else { // 点击popover部分 执行的逻辑 console.log('点击popover部分 执行的逻辑'); } } @Watch('visible') onVisibleChange(newValue: boolean) { // 当 popover 显示时 执行的逻辑 // 显示 弹出框,将弹出框节点放到 body 子节点的最后 // 改变弹出框样式,使其出现在相对按钮合适的位置 if (newValue) { console.log('打开状态'); this.$nextTick(() => { // 将 弹出框 放到body 里 this.positionPop(); // 使 添加监听在 点击事件冒泡 之后 执行 setTimeout(() => { // 给 document 添加 click 事件监听 this.listenToDocument(); }); }); } else { // 当 popover 隐藏时 执行的逻辑 console.log('关闭'); } } } </script> <style lang="scss" scoped>...</style>
@Watch('visible')监听数据this.visible的变化visible重构改成isVisible
解决重复叠加监听与重复多次移除监听的bug
是否可以在
created时,执行this.listenToDocument()
- 不可
- 当页面中有多个
popover组件时,每次创建就添加监听会造成不必要的性能浪费
- 当页面中有多个
- 而监听事件正确的做法是 即用即添,用完即毁
- 当点击弹出一个消息框,就添加一个监听
- 当消息框关闭时,立即清除
收拢关闭操作的入口
- 高内聚
- 抽象方法
- 将多个表达相同逻辑的代码抽象成一个方法,在处理该逻辑的时候调用这个方法
实例代码的
closeEvent(),将收尾的逻辑聚拢到一个方法中,有两个操作- 关闭显示
this.isVisible = false; 移除监听
this.rmListenerToDocument();1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132<template> <div ref="popover" class="popover" @click="togglePop"> <div ref="contentWrapper" class="content-wrapper" v-if="isVisible"> <slot name="content"></slot> </div> <div class="triggerWrapper" ref="triggerWrapper"> <slot>button</slot> </div> </div> </template> <script lang="ts"> import {Component, Vue, Watch} from 'vue-property-decorator'; @Component export default class VuePopover extends Vue { name = 'VuePopover'; isVisible = false; // 定位 popover 显示位置 positionPop() { // 获取 弹出消息框节点 的引用,放到 body 子节点的最后 document.body.appendChild(this.$refs?.contentWrapper as Node); // 获取 按钮元素 左上顶点的位置坐标 top, left const {top, left} = (this.$refs.triggerWrapper as Element) .getBoundingClientRect(); // 设置 弹出消息框节点 的行内样式,使其定位到 按钮元素 上方 (this.$refs.contentWrapper as any).style.top = `${top + window.scrollY}px`; (this.$refs.contentWrapper as any).style.left = `${left + window.scrollX}px`; } // 定义一个点击事件的回调函数 closeHandler(e: Event) { // 点击的目标对象 是否存在于 popover 包裹div中 const hasPopover = ((this.$refs.contentWrapper as Element) ?.contains(e.target as Node)); // 如果 点击的目标对象 不存在于 包裹弹出框的div中 即 点击了document if (!hasPopover) { this.closeEvent(); } else if (this.$refs.popover && (hasPopover || this.$refs.popover === e.target)) { return; } } listenToDocument() { document.addEventListener('click', this.closeHandler); } rmListenerToDocument() { document.removeEventListener('click', this.closeHandler); } // 关闭 弹出框 销毁事件监听 closeEvent() { this.isVisible = false; this.rmListenerToDocument(); } openEvent() { this.isVisible = true; this.onShowPopover(); } // 点击按钮 执行的方法:切换显示/隐藏 popover togglePop(event: Event) { // 点击按钮部分 执行的逻辑 if ((this.$refs.triggerWrapper as HTMLElement) ?.contains(event.target as Node)) { // 切换显示/隐藏 popover this.isVisible = !this.isVisible; } else { // 点击popover部分 执行的逻辑 } } onShowPopover() { this.$nextTick(() => { // 显示 弹出框,将弹出框节点放到 body 子节点的最后 // 改变弹出框样式,使其出现在相对按钮合适的位置 this.positionPop(); // 使 添加监听在 点击事件冒泡 之后 异步执行 setTimeout(() => { // 给 document 添加 click 事件监听 this.listenToDocument(); }); }); } // 监听 this.isVisible 状态变化 执行对应的逻辑 @Watch('isVisible') onVisibleChange(newValue: boolean) { if (newValue) { // 当 popover 显示时 执行的逻辑 this.onShowPopover(); } else { // 当 popover 隐藏时 执行的逻辑 return; } } } </script> <style lang="scss" scoped> .popover { display: inline-block; vertical-align: top; position: relative; .triggerWrapper { } } .content-wrapper { display: block; position: absolute; box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); padding: 2px; transform: translateY(-100%); background-color: white; } </style>
- 关闭显示
设置触发部分
<span class="triggerWrapper" ref="triggerWrapper">...的样式- 将默认行内显示的
<span>标签添加样式display: inline-block;
- 将默认行内显示的
小结 popover 的三个问题
overflow: hidden;由body.appendChild解决- 处理
window.scrollX和window.scrollY滚动偏移量
- 处理
- 关闭重复n次,分开处理逻辑
- 职责明确
document只管外面popover只管里面
- 未取消监听 document,由收拢
close的逻辑解决
添加支持四个方位功能
添加消息框的三角指向样式
MDN filter: drop-shadow()代替box-shadow- 需要配合
background-color: white;
- 需要配合
添加消息框的四个方向 的外部数据
可以看到,去掉了
transition和margin-top属性的popover和按钮的左上角是对齐的
区分位置样式
|
|
使用表驱动编程重构
- 创建一张表
const positionList = {...}分别对应四个位置'position-top': {...},'position-bottom': {...},'position-left': {...},'position-right': {...},
- 设置每个位置的
top和left属性 - 提取重复代码
(contentWrapper as HTMLElement).style.top = ...(contentWrapper as HTMLElement).style.left = ...
- 去除
if的判断,简化逻辑为一一对应
VuePopover.vue
|
|
抽出部分
VuePopover.scss
|
|
动态监听事件vue2.6+
- 动态绑定事件
@[event]="eventFn"v-on="{[popUp]: [openEvent, clearTimer], [popDown]: startTimer}"
一次绑定多个事件
v-on="{mouseover: this.clearTimer, mouseout: this.startTimer}"- 可使用计算属性简化为
v-on="multiEvent" 不可使用缩写
@1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88<template> <div ref="popover" class="popover" v-on="{[popUp]: [openEvent, clearTimer], [popDown]: startTimer}" @click="togglePop"> <div ref="contentWrapper" class="content-wrapper" v-if="isVisible" v-on="multiEvent" :class="{[`position-${position}`]: true}"> <slot name="content" :closeEvent="closeEvent"></slot> </div> <span class="triggerWrapper" ref="triggerWrapper" v-on="multiEvent"> <slot>button</slot> </span> </div> </template> <script lang="ts"> import {Component, Prop, Vue, Watch} from 'vue-property-decorator'; @Component export default class VuePopover extends Vue { name = 'VuePopover'; isVisible = false; timer: number | null = null; // prop: [autoCloseDelay, position, trigger] @Prop({...}) autoCloseDelay!: false | number; @Prop({...}) position!: 'top' | 'bottom' | 'left' | 'right'; clearTimer() { clearTimeout(this.timer || undefined); } startTimer() { if (this.autoCloseDelay) { this.timer = setTimeout(() => { this.closeEvent(); }, this.autoCloseDelay); } } get multiEvent() { return {mouseover: this.clearTimer, mouseout: this.startTimer}; } get popUp() { return this.trigger === 'hover' ? 'mouseenter' : ''; } get popDown() { return this.trigger === 'hover' ? 'mouseleave' : ''; } // 定位 popover 显示位置 positionPop() {...} // 定义一个点击关闭事件的回调函数 closeHandler(e: Event) {...} listenToDocument() {...} rmListenerToDocument() {...} // 关闭 弹出框 销毁事件监听 closeEvent() {...} openEvent() {...} // 点击按钮 执行的方法:切换显示/隐藏 popover togglePop(event: Event) {...} onShowPopover() {...} // 监听 this.isVisible 状态变化 执行对应的逻辑 @Watch('isVisible') onVisibleChange(newValue: boolean) {...} } </script> ...
添加支持可选的click和hover两种触发popover方式
Popovers.vue添加外部数据trigger默认为hover
|
|
VuePopover.vue
|
|
在弹出框中加上按钮,并且传递属性参数
在弹出框加上关闭按钮,并将组件中的关闭方法传给按钮,使按钮可以调用组件传来的 API
使用具名插槽
- 在组件的
slot标签中,提供属性name,用来定义具名插槽- 不带
name的匿名插槽<slot>出口会带有隐含的name属性“default”
- 不带
- 使用组件时,即向具名插槽提供内容时,在
template标签上使用v-slot指令- 以
v-slot:xxx的参数的形式提供其名称xxx <template>元素中的所有内容都将会被传入对应name属性插槽- 组件标签内,带有 v-slot 的 标签 之外的内容都会被视为默认插槽的内容
- 即与
<template v-slot:default>...</template>等效
- 以
使用 作用域插槽 ,实现传递组件的方法到<slot>中
- 在组件的
slot标签中,在slot上绑定组件的数据<slot v-bind:data="data">...后备内容...</slot>匿名插槽- 缩写
<slot :data="data">...后备内容...</slot> <slot name="xxx" :data="data">...后备内容...</slot>具名插槽<slot>元素上绑定的 attribute 被称为 插槽 prop
- 使用组件时,写在组件内的
template标签中使用带值的v-slot来定义提供的 插槽 prop 的名字<template v-slot:default="slotProps">...</template>匿名插槽- 缩写
<template #default="slotProps">...</template> <template #xxx="slotProps">...</template>具名插槽- 插槽内容(可以为一个子组件)能够访问组件中才有的数据
- 即向插槽传值
- 独占默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确,会导致警告
- 模板内容中编译规则
- 父级组件模板里的所有内容都是在父级作用域中编译的
- 子组件模板里的所有内容都是在子作用域中编译的
例如
- 父组件可以访问到数据 faData,提供的内容是在父级渲染的
- 插槽中的子组件无法直接访问 faData
- 为了让 faData 在父级的插槽内容在子组件中可用
- 可以将 faData 作为
元素的一个 attribute 属性绑定 - 在
<slot :faData="faData"></slot>上绑定属性
- 可以将 faData 作为
- 绑定在
元素上的 attribute 被称为 插槽 prop - 在父级作用域中,可使用带值的 v-slot 来定义提供的插槽 prop 的名字
<template v-slot:default="anySlotPropsNameLikefaData">...</template>
使用 ES2015 解构来传入具体的插槽 prop
<template v-slot:xxx="{ data }">...- 写在
<template></template>中,可以缩写具名插槽v-slot:xxx缩写为#xxx...<template #xxx></template>...- 默认插槽
...<comp #default="{ data }"</comp>...
- 可以从外层组件获取数据
VuePopover.vue
|
|
<slot name="content" :closeEvent="closeEvent"></slot>- 在具名插槽
content中绑定数据:closeEvent="closeEvent"
Popovers.vue展示组件
|
|
<template #content="{closeEvent}">...</template>- 解构数据
#content="{closeEvent}" - 使用解构出的方法
<VueButton @click="closeEvent">关闭</VueButton>
- 解构数据
参考
封装为指令
|
|
参考
- 代码链接:popover.vue