背景
el-table
对于列的居左、居右(即<el-table-column fixed='left'/>
)功能实现 采用的方式是将多个el-table
进行拼凑,也就是说设置了居左、居右后,将出现三个el-table
, 分别为 【居左】、【中间滚动区域】、【居右】 的 el-table
拼凑出浮动功能。这将可能引起重复渲染组件带来的bug。此文章将记录在此背景下的两个需求引发的问题及解决方案。
需求一:对列是复选框的表头插入按钮,
问题:
1、当列为复选框时存在一个特殊性,其列的头部存在一个全选功能的checkBox,采用插槽方式无法直接替换内容
<el-table-column
label-class-name="table-checkbox"
:key="`selection`"
type="selection"
fixed="left"
>
<!-- 此插槽无效 -->
<template v-slot:header>
</template>
</el-table-column>
2、开启列 fixed 后 将导致出现重复的 el-table,需要所有el-table 都插入才行
解决思路:
1、采用 js 获取 el-talbe 的复选框列表头 操作 dom
2、对每个找到的el-table 都进行需要的dom操作
解:
复选框列上添加自定义class名 table-checkbox
<el-table-column
label-class-name="table-checkbox"
:key="`selection`"
type="selection"
fixed="left"
>
</el-table-column>
js 中检索 此 class 名且属于表头的元素
setBtn() {
// 创建元素方法
function createElement(icon, callback) {
const button = document.createElement('i')
button.className = `${icon}`
let timeout
button.onclick = function ($event) {
$event.preventDefault()
clearTimeout(timeout)
timeout = setTimeout(callback, 0)
}
return button
}
this.$nextTick(() => {
// 获取该 el-table ref
const tableEl = this.$refs[`test_table`]
if (!tableEl) return
// el-table 可能存在多个表用于处理不同场景,因此每个 div.cell.table-checkbox 都要同步设置
// 检索 此 class 名且属于表头的元素
const headCellList = tableEl.$el.querySelectorAll(
'th > div.cell.table-checkbox'
)
if (!isEmpty(headCellList)) {
// 循环所有的元素进行插入
headCellList.forEach(headCell => {
// 0、创建div包裹按钮
let box = headCell.querySelector('.heard-search-icons')
if (!box) {
box = document.createElement('div')
box.className = 'btn-box'
}
// 1、按钮
let searchEl = box.querySelector('.icon-lcdp-Search')
if (!searchEl) {
// 不存在,创建此元素 并绑定上方法 load
searchEl = createElement.bind(this)(
'el-icon-star-on',
this.load
)
box.appendChild(searchEl)
}
// 按需控制隐藏显示
if (true) {
searchEl.hidden = false
} else {
// 未开启 隐藏
searchEl.hidden = true
}
// 3、插入包裹的 box
box && headCell.appendChild(box)
})
}
})
},
Complate the task 1
需求二:列开启展开行后插入组件
问题:
问题1、 因存在别的列开启了fixd ,导致出现多个table 且在展开行时组件被渲染多次。
解决思路:
解1、在需要插入展开行的组件内部created()
时判断其父级table class 是否 存在 ‘fixed’ 标识,及 this.$parent.$el.offsetParent.className
方式获取,被居左、居右的table class会携带 ‘fixed’,根据此办法判断是否进行 v-if 判断是否加载组件内的内容。
思路参考:https://github.com/ElemeFE/element/issues/12177
问题2、此方式同时也带来新问题:即使滞空了另外两个fixed的 table 的展开行内容,也需要保持三个table 的展开行高度一致,否则展开后,居左、居右的table 其列与 中间内容区的table 不对齐。如下图:
【图待补充】
此问题可采用解决方案:在行内要显示的组件内部进行渲染控制
解2、对不属于fixed 的table内的组件进行监听自身高度变化,变化后同步高度给另外两个fixed 下的组件,保证三个table 展开后高度保持一致。
解:
<!-- el-table -->
<el-table-column type="expand" width="55">
<template slot-scope="{ row, $index }">
<!--某个组件-->
<demoCompent />
</template
</el-table-column>
<!--展开行内组件 demoCompent HTML-->
<template>
<div ref="expandRowBox" class="draggable-component-box">
<!--此处空白区高度占位 也可以采用直接设置在draggable-component-box 的高度样式里实现-->
<div
v-if="!isLive"
:style="`height:${
expandRowHeight?.val && !isLive
? expandRowHeight?.val + 'px'
: undefined
}`"
>
占位
</div>
<!-- 本要渲染的组件 -->
<div v-else>
<actualComponent />
</div>
</template>
<!--展开行内组件 demoCompent JS -->
props: {
// 展开行内容高度,传入引用地址若改变将导致组件被重新渲染,导致监听高度变化出现无线循环,采用对象方式接收高度不改变引用地址可避免此问题
expandRowHeight: {
type: Object,
default: () => ({
val: 0
})
},
},
data(){
return {
// 是否允许存在组件
isLive: true,
// 内容区el
contentEl: '',
// 高度监听器
resizeObserver: '',
}
},
created() {
<!--解1 实现代码-->
// fix table 不再显示组件,用于解决table fixed 出现多个table重复渲染组件
if (this.isFixTable()) {
this.isLive = false
}
},
mounted() {
<!--解2 实现代码-->
if (!this.isFixTable()) {
// 非fix Table 的展开行才监听高度变化
this.observerHeight()
}
},
beforeDestroy() {
this.destroyObserver()
},
methods(){
// 判断当前组件位置是否在 fixed的 el-table中
isFixTable() {
const className = this.$parent.$el.offsetParent.className
return className.includes('fixed')
},
// 1、监听高度变化
observerHeight() {
// 获取要监听高度变化的元素
this.contentEl = this.$refs.expandRowBox
this.emitHeight()
// 创建 ResizeObserver 实例,并传入回调函数
this.resizeObserver = new ResizeObserver(
throttle(() => this.emitHeight(), 1000)
)
// 开始监听元素的大小变化
this.contentEl && this.resizeObserver.observe(this.contentEl)
},
// 2、向外传递有效组件内的高度,同步其他table行内高度
emitHeight() {
if (!this.isFixTable()) {
this.$emit('setHeight', this.$el.offsetHeight)
// 传递给外部后,由外部接收 this.$el.offsetHeight 的值后重新赋值给本组件 expandRowHeight.val 进行逻辑判断渲染另外两个fixed 的组件的内容高度,形成闭环。
}
},
// 销毁监听
destroyObserver() {
if (this.resizeObserver) {
// 在组件销毁前停止监听
this.contentEl && this.resizeObserver.unobserve(this.contentEl)
}
},
}