背景
在最开始使用 el-dialog 的时候发现,它会在 body 标签下插入一个 div 遮罩层,而不是像一般组件一样,在 vue 的组件内容 <div id="app"></div> 内进行层级嵌套内容,自己发现后也没有去细追这样设计的原因
然后有次面试就被问了......
现在回来清算旧账
dialog 为什么在 body 层
html 文档中,子div 的大小常常由 父div 限制,而遮罩层因为其特殊性,常常需要占满全屏空间,所以将 遮罩层的 div 插入到 vueApp 的 div 同级目录下,也就是 body 层下
子 div 突破父 div 大小限制
这里使用绝对布局来实现
html 代码:
<html>
<div class="div1">
<div class="div2">
<div class="div3">
</div>
</div>
</div>
</html>css 代码:
<style>
.div1{
border-style:solid;
border-width:1px;
width: 400px;
height: 300px;
position:relative;
}
.div2{
border-style:solid;
border-width:1px;
width: 200px;
height: 100px;
margin-left:50px;
margin-top: 50px;
}
.div3{
width:100%;
height:100%;
background-color:red;
position:absolute;
margin-left:-50px;
margin-top: -50px;
}
</style>div3 受 div2 大小限制,用 position 突破,在 div2 前添加 div1,div1 使用 position:relative;
div3 使用 position:absolute;
由于 div2 没有使用 position,所以 div3 width:100%;height:100%; 时跟随 div1 大小,但是位置还是 div2 的起始位置,所以 div3 使用 margin-left:-50px;margin-top: -50px; 就改变到 div1 的相对位置
popover 和 tooltip 为什么在 body 下
相比于 dialog,element-ui 的 popover 和 tooltip 也是默认在 body 下创建的
为什么?
以 tooltip 为例,在.vue 文件中真正的弹层是放在组件节点下面的,使用 ref="popper" 来引用
在初始化的时候通过 this.$refs.popper 传递给 new Popper 创建,这样的话所有的弹层都在父节点下,会导致在 table 中弹出的层无法突破 z-index 的限制被 overflow 掉,展示不全
但在真实业务场景中,这两个组件在 body 下有很多界面不友好的问题,这个时候一般都将组件设置在特定的 父div 下
指定 tooltip 的生成 dom 位置
<template>
<div>
<el-tooltip
placement="top"
:append-to-body="false" <!-- 尤其重要参数 -->
content="你好ToolTip"
ref="mypop"> <!-- 为方便寻找,使用ref直接拿 -->
<el-button>提示</el-button>
</el-tooltip>
<!-- 希望将tooltip的弹出部分内容的dom放在这里面 -->
<div ref="here"></div>
</div>
</template>
<script>
export default {
mounted() {
// 在mounted的时候只需要使用这句话。
this.$refs.here.appendChild(
this.$refs.mypop.popperVM.$el
)
}
}
</script>指定 dropdown
也是利用 refs 的特性
<template>
<div>
<el-dropdown>
<span class="el-dropdown-link">
下拉菜单<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<!--同样是将append-to-body参数设置为false-->
<el-dropdown-menu slot="dropdown" :append-to-body="false" ref="mydropd">
<el-dropdown-item>黄金糕</el-dropdown-item>
<el-dropdown-item>狮子头</el-dropdown-item>
<el-dropdown-item>螺蛳粉</el-dropdown-item>
<el-dropdown-item disabled>双皮奶</el-dropdown-item>
<el-dropdown-item divided>蚵仔煎</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div ref="here"></div>
</div>
</template>
<script>
export default {
mounted() {
this.$refs.here.appendChild(
this.$refs.mydropd.popperElm
)
}
}
</script>实现原理
通过阅读这两个组件的源代码可以发现它们都是 minxin 了 vue-popper 这个公共类,使他们都具备这样的弹出式窗口的功能。部分关键源代码如下:
// dropdown-menu.vue 的部分源码
import Popper from 'element-ui/src/utils/vue-popper';
export default {
name: 'ElDropdownMenu',
componentName: 'ElDropdownMenu',
mixins: [Popper], // <=== 这里
props: {
visibleArrow: {
type: Boolean,
...而 vue-popper 组件提供了 appendToBody 参数,它默认值是 true,也就是默认就会把弹出的内容添加到 body 根节点。部分重要代码入下:
// vue-popper.js 的部分代码
methods: {
createPopper() {
...
if (!popper || !reference) return;
if (this.visibleArrow) this.appendArrow(popper);
// 这里可以看到,根据参数来决定是否要把弹出的内容显示添加到body根节点。
if (this.appendToBody) document.body.appendChild(this.popperElm);
...一旦我们将 appendToBody 参数设置为了 false,那弹出的内容根本就不会出现在界面上了,所以我们需要自己在 mounted 生命周期函数里手动将对应的弹出内容添加到界面上,经过简单的代码阅读,不难发现:
对于 tooltip 组件,内部的 popperVM.$el 就是弹出内容。
对于 dropdown 组件,内部的 popperElm 就是弹出内容。
只需要将它们添加到我们自己想要的地方即可。
而 element-ui 里有很多组件都 minxin 了 vue-popper,应该都可以使用类似的方法去将内容放在指定位置。下面简单罗列了那些组件使用了 vue-popper:
submenu、autocomplete-suggestions、cascader、picker-dropdown、picker、select-dropdown、filter-panel、popover
参考
使用 position突破父元素大小限制_Incomplete
tooltip弹层为什么不放在body下面,或者是否可以全部放到body下面? · Issue #296 · ElemeFE/element · GitHub
【Microanswer】-elementui将tooltip和dropdown显示到指定dom而非body根节点