ElTable实现空单元格自动填充占位符

前端杂事 专栏收录该内容
205 篇文章 0 订阅

ElTable实现空单元格自动填充占位符‘–’

根据前端开发规范及UE建议,考虑给表单的空单元格插入占位符‘–’,一开始的想法很简单,在el-table-column中渲染时对传入的data进行判断即可,相关代码如下:

<el-table-column label="按键" width="100px">
    <span class="c-one-line did-desc-text">
      {{scope.row.didDesc || $nullValue}}
  	</span>
</el-table-column>

但是问题来了,每一个表格都要加上这么一个 ||进行判断,工作量比较大,需要思考一个统一的解决方案。

通过DOM操作实现

考虑CSS选择器:empty,参考MDN的介绍:

:empty CSS 伪类 代表没有子元素的元素。子元素只可以是元素节点或文本(包括空格)。注释或处理指令都不会产生影响。

于是,可以这样选择为数据为空的单元格

.el-table__body td > div *:empty

使用通配符的原因是,我们并不清楚待插入的dom节点到底位于div下的哪一个子节点,因此只能通过通配符进行全量查找,配合部分标签和class白名单的方式来插入占位符。

部分标签白名单解释:(都不需要插入占位符)

  • i标签,一般用于生成图标
  • input,一般用于按钮,下拉框等表单元素
  • usesvg图片中子标签

部分class白名单:

  • el-scrollbar__thumb,悬浮框或者其他组件中的滚动条实现,对于此空标签不需要处理

通过:empty进行节点选择存在一个问题:会忽略掉全是空白字符的标签,对于这些标签理论上对于用户而言也相当于空单元格。查询有没有解决这一问题的css选择器,发现确实有:blank,参考MDN的介绍:

:blank CSS 伪类选择器 用于匹配如下节点

  1. 没有子节点;

  2. 仅有空的文本节点;

  3. 仅有空白符的文本节点.

非常匹配我们的需求,(可人生总是充满了但是)但是这个选择器目前没有被任何浏览器支持。

所以,只能通过Javascript进行文本节点内容判空处理了,以下为相关源码:

import {Table} from 'element-ui';
import {NULL_VALUE} from '~/common/constant';

// 给空单元格加上'--'占位
(Table as any).mixins = ((Table as any).mixins || []).concat({
    updated(this: any) {
        if (this.$el) {
            const table = this.$el.querySelector('.el-table__body');
            table.querySelectorAll('td > div *:empty')
                .forEach((it: HTMLElement) => {
                    // 对表格中悬浮框的滚动条不做处理
                    if (!it.getAttribute('class')?.includes('el-scrollbar__thumb')) {
                        if (!['i', 'input', 'use'].includes(it.tagName.toLocaleLowerCase())) {
                            it.innerText = NULL_VALUE;
                        }
                    }
                });
            // 兼容el-table-column中template没子元素且跨行处理时textContent为空格的情况
            table.querySelectorAll('td > div')
                .forEach((it: HTMLElement) => {
                    if (!it.innerText.trim() && !it.children.length) {
                        it.innerText = NULL_VALUE;
                    }
                });
        }
    }
});

其实文章写到这差不多也该结束了,至少在2020-04-01之前是的。

可是却在动笔前,在2020-04-01这天发现了这种实现方式的一个非常严重的bug,通过js修改过dominnerText属性之后,尽管这个dom节点之前被Vue数据绑定过,可是节点被手动修改之后,当Vue数据再发生变化时这个dom节点却依然失去了响应式效果,也就是说Vuedom更新机制被破坏了。

冥思苦想

为了了解Vuedom更新机制被破坏的原因,需要深入了解Vue相关源码,带着“Vue是如何更新dom”这个问题开始寻找Vue的相关处理逻辑。

通过调试Vue源码(src/core/vdom/patch.js patchVnode L548)发现在进行文本节点赋值的时候,使用了setTextContent方法:(src/platforms/web/runtime/node-ops.js L53),对于传入的node节点,设置文本属性:

export function setTextContent (node: Node, text: string) {
  node.textContent = text
}
// var elm = vnode.elm = oldVnode.elm;

在这里插入图片描述

而当我们执行了dom.innerText为文本节点赋值之后
在这里插入图片描述

可以发现dom节点中的text节点发生变化,也就是说Vue中oldVnode中保存的elm对象已经不是最新的dom引用了,所以在正常逻辑下执行到nodeOps.setTextContent的时候理论上会更新到正常的dom节点,而手动修改dom节点之后Vue并不知道这个节点已经不存在了,所以看起来这个被修改后的节点失去了‘响应式’。

怎么办

手动修改dom节点之后如何通知Vue更新内部维护的Vnode节点对象呢?这边没有想到比较好的方法,不过为了实现空单元格自动填充占位符的功能,也许可以直接修改下ElTable的相关源码。

看到packages/table/src/table-column.js L146 setColumnRenders方法(L170),这里我们可以看到默认使用了defaultRenderCell方法:

export function defaultRenderCell(h, { row, column, $index }) {
  const property = column.property;
  const value = property && getPropByPath(row, property).v;
  if (column && column.formatter) {
    return column.formatter(row, column, value, $index);
  }
  return value;
}

这里返回单元格展示的值,可以在这里侵入修改:

return value || '--';

而对于scopedSlot这种情况呢?

看到setColumnRenders方法中的这一段:

column.renderCell = (h, data) => {
  let children = null;
  if (this.$scopedSlots.default) {
    children = this.$scopedSlots.default(data);
  } else {
    children = originRenderCell(h, data);
  }
  const prefix = treeCellPrefix(h, data);
  const props = {
    class: 'cell',
    style: {}
  };
  if (column.showOverflowTooltip) {
    props.class += ' el-tooltip';
    props.style = {width: (data.column.realWidth || data.column.width) - 1 + 'px'};
  }
  return (<div { ...props }>
    { prefix }
    { children }
  </div>);
};

children对象即为被Vue解析过的Vnode对象数组,也许可以直接通过修改Vnode对象的属性来达到我们想要的效果,分析一下Vnode的属性列表:

Vnode属性说明:
* children 是当前 vnode 的子节点(VNodes)数组
* data 是当前 vnode 代表的节点的各种属性,是 createElement() 方法的第二个参数
* elm 是根据 vnode 生成 HTML 元素挂载到页面中后对应的 DOM 节点
* tag 是当前 vnode 对应的 html 标签
* text 是当前 vnode 对应的文本或者注释

修改下text属性即可:

function setText(children: any) {
    children.forEach((child: any) => {
        if (child.children && child.children.length) {
            setText(child.children);
        }
        // 不处理Vue组件的text属性
        else if (!(child.tag && child.tag.includes('vue-component'))) {
            child.text = child.text || NULL_VALUE;
        }
    });
}

全部修改源码实现如下,注意此代码文件应当位于Vue.use(ElementUI)之前

/**
 * @file element-ui部分组件逻辑补充
 * @author zoubo01<zoubo01@baidu.com>
 */

import {TableColumn} from 'element-ui';
import {NULL_VALUE} from '~/common/constant';

// *** element-ui 源码start src/utils/util ***
function getPropByPath(obj: any, path: any, strict?: any) {
    let tempObj = obj;
    path = path.replace(/\[(\w+)\]/g, '.$1');
    path = path.replace(/^\./, '');

    let keyArr = path.split('.');
    let i = 0;
    for (let len = keyArr.length; i < len - 1; ++i) {
        if (!tempObj && !strict) {
            break;
        }
        let key = keyArr[i];
        if (key in tempObj) {
            tempObj = tempObj[key];
        }
        else {
            if (strict) {
                throw new Error('please transfer a valid prop path to form item!');
            }
            break;
        }
    }
    return {
        o: tempObj,
        k: keyArr[i],
        v: tempObj ? tempObj[keyArr[i]] : null
    };
}
// *** element-ui 源码end src/utils/util ***

// *** element-ui 源码start packages/table/src/config.js ***
function defaultRenderCell(h: Function, {row, column, $index}: any) {
    const property = column.property;
    const value = property && getPropByPath(row, property).v;
    if (column && column.formatter) {
        return column.formatter(row, column, value, $index);
    }
    // 插入占位符 **** 源码修改点 ****
    return value || NULL_VALUE;
}

function treeCellPrefix(h: Function, {row, treeNode, store}: any) {
    if (!treeNode) {
        return null;
    }
    const ele = [];
    const callback = function (e: any) {
        e.stopPropagation();
        store.loadOrToggle(row);
    };
    if (treeNode.indent) {
        ele.push(<span class="el-table__indent" style={{'padding-left': treeNode.indent + 'px'}}></span>);
    }
    if (typeof treeNode.expanded === 'boolean' && !treeNode.noLazyChildren) {
        const expandClasses = ['el-table__expand-icon', treeNode.expanded ? 'el-table__expand-icon--expanded' : ''];
        let iconClasses = ['el-icon-arrow-right'];
        if (treeNode.loading) {
            iconClasses = ['el-icon-loading'];
        }
        ele.push(<div class={ expandClasses } on-click={ callback }>
            <i class={ iconClasses }></i>
        </div>);
    }
    else {
        ele.push(<span class="el-table__placeholder"></span>);
    }
    return ele;
}
// *** element-ui 源码end packages/table/src/config.js ***

/**
 * 为生成的Vnode插入文本占位符
 * Vnode属性说明:
 * children 是当前 vnode 的子节点(VNodes)数组
 * data 是当前 vnode 代表的节点的各种属性,是 createElement() 方法的第二个参数
 * elm 是根据 vnode 生成 HTML 元素挂载到页面中后对应的 DOM 节点
 * tag 是当前 vnode 对应的 html 标签
 * text 是当前 vnode 对应的文本或者注释
 * @param children
 */
function setText(children: any) {
    children.forEach((child: any) => {
        if (child.children && child.children.length) {
            setText(child.children);
        }
        // 不处理Vue组件的text属性
        else if (!(child.tag && child.tag.includes('vue-component'))) {
            child.text = child.text || NULL_VALUE;
        }
    });
}

(TableColumn as any).methods.setColumnRenders = function (column: any) {
    // *** element-ui 源码start packages/table/src/table-column.js ***

    // renderHeader 属性不推荐使用。
    if (this.renderHeader) {
        console.warn('[Element Warn][TableColumn]Comparing to render-header, scoped-slot header is easier to use. We recommend users to use scoped-slot header.');
    }
    else if (column.type !== 'selection') {
        column.renderHeader = (h: Function, scope: any) => {
            const renderHeader = this.$scopedSlots.header;
            return renderHeader ? renderHeader(scope) : column.label;
        };
    }

    let originRenderCell = column.renderCell;

    if (column.type === 'expand') {
        // 对于展开行,renderCell 不允许配置的。在上一步中已经设置过,这里需要简单封装一下。
        column.renderCell = (h: Function, data: any) => {
            console.log('render cell expand', data);
            return (<div class='cell'>
                { originRenderCell(h, data) }
            </div>);
        };
        this.owner.renderExpanded = (h: Function, data: any) => {
            return this.$scopedSlots.default
                ? this.$scopedSlots.default(data)
                : this.$slots.default;
        };
    }
    else {
        originRenderCell = originRenderCell || defaultRenderCell;
        // 对 renderCell 进行包装
        column.renderCell = (h: Function, data: any) => {
            let children = null;
            if (this.$scopedSlots.default) {
                children = this.$scopedSlots.default(data);
            }
            else {
                children = originRenderCell(h, data);
            }
            const prefix = treeCellPrefix(h, data);
            const props = {
                class: 'cell',
                style: {}
            };
            if (column.showOverflowTooltip) {
                props.class += ' el-tooltip';
                props.style = {width: (data.column.realWidth || data.column.width) - 1 + 'px'};
            }
            // 插入空占位符 **** 源码修改点 ****
            setText(children);
            return (<div { ...props }>
                {prefix}
                {children}
            </div>);
        };
    }
    return column;
    // *** element-ui 源码end packages/table/src/table-column.js ***
};
  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值