Vue3利用h和原生js修改DOM渲染之后的样式

223 阅读6分钟

修改表格的样式

最近有一项需求,需要修改表格中固定的最后一列即操作按钮列的样式:在按钮之间加一条分割线,当按钮大于3个时,保留前两个按钮,后续按钮放入更多的下拉菜单中,类似下图效果:

表格格式.png

DOM渲染之前修改

刚一听感觉很简单,不就是改样式,直接取按钮个数,然后写两套样式,根据按钮数量直接绑定对应样式完事,几分钟就能写完,然而一结合业务分析就感觉不是想的那么简单!

首先业务代码中的操作列是用插槽写的,然后其中的按钮有些是前端写死的、有些是后端返回的,并且最要命的是每个按钮都有一大堆自己的显隐逻辑,需要根据不同的条件显示按钮,类似下面的伪代码:

if(条件1 && 条件2) return 按钮1
if(条件1 && 条件2 && 条件3) return 按钮2
if(条件1 && 条件3) return 按钮3
if(条件2 && 条件4) return 按钮4
if(条件5) return 按钮5

这就导致同一个表格组件在不同的条件下显示的按钮个数不一样,且表格不同行的按钮个数也可能不一样。一开始我还想梳理下每个按钮的显隐逻辑,试图合并相同的条件以总结出按钮显隐的所有情况,但最后实在被一大堆条件绕晕了。请教老师傅后给出了一个方法,不管哪种情况会有哪些按钮,直接暴力解法:等页面渲染完毕,然后再取操作列的DOM,根据其中的按钮个数再修改样式。

DOM渲染之后修改

虽说直接操作DOM听起来有点暴力,但确实是目前最好的解法了,且也不是那么容易实现的(最好还是将按钮的逻辑全放在后端处理,只给前端返回需要展示的按钮),下面用一个demo来模拟一下。

1.创建表格

<el-table>简单模拟一个表格,用setTimeout模拟数据请求,操作列给4个按钮:

// 核心代码
setup() {
  const tableData = ref<any[]>([]);
  setTimeout(() => {
    tableData.value = [
      {
        date: "2016-05-03",
        name: "Tom",
      },
      {
        date: "2016-05-02",
        name: "Tom",
      },
      {
        date: "2016-05-04",
        name: "Tom",
      },
    ];
  }, 500);
  return () => (
    <div>
      <el-table data={tableData.value}>
        <el-table-column prop="date" label="Date" width="180" />
        <el-table-column prop="name" label="Name" width="180" />
        <el-table-column
          prop="operation"
          label="操作"
          fixed="right"
          v-slots={{
            default: () => (
              <>
                <el-link type="primary">新增</el-link>
                <el-link type="primary" style={{ marginLeft: "5px" }}>
                  删除
                </el-link>
                <el-link type="primary" style={{ marginLeft: "5px" }}>
                  修改
                </el-link>
                <el-link type="primary" style={{ marginLeft: "5px" }}>
                  查找
                </el-link>
              </>
            ),
          }}
        />
      </el-table>
    </div>
  );
},

按需求要将修改和查找按钮放在更多下拉菜单中,并在显示的按钮两两之间添加分割线,现在的页面如下:

简单表格.png

2.获取表格操作列的DOM

操作列DOM.png

因操作列固定在右边,所以可以利用该属性来获取其下的单元格DOM,cellList为操作列所有行的单元格DOM节点列表,通过遍历对每行进行固定操作,可以取到单元格cell中的所有孩子元素的个数即按钮数量:

const cellList: NodeList = document.querySelectorAll(
  "td.el-table-fixed-column--right > div.cell"
);
cellList.forEach((cell) => {
  const cellElementList: HTMLCollection = (cell as HTMLElement).children;
  const length = cellElementList.length;
  ...
}

3.按钮个数大于等于2的情况

创建分割线DOM并从第2个按钮开始插入到所有按钮前面:

if (length >= 2) {
  for (let i = 1; i < cellElementList.length; i += 2) {
    const divider = document.createElement("div");
    divider.setAttribute("class", "el-divider el-divider--vertical");
    divider.setAttribute("role", "separator");
    divider.setAttribute("style", "--el-border-style: solid;");
    cell.insertBefore(divider, cellElementList[i]);
  }
}

这里i += 2 是因为类型为HTMLCollectioncellElementList是实时更新的,当下一次获取cell的孩子元素时分割线也被包含在内,所以需要+2跳过;分割线divider用的是<el-divider />分割线组件,将其属性设置和<el-divider />一样就可得到相应的渲染效果,属性可以去官网上查看其DOM结构:

divider属性.png

此时页面如下:

添加分割线后的操作列.png

4.按钮个数大于等于4的情况

此情况包含上述情况,已进行分割线的插入操作,此时将修改、查找按钮和分割线全部移除,按钮保存到数组中;再创建更多下拉菜单并渲染;最后将保存的按钮放到下拉菜单项中:

if (length >= 4) {
  // 1.将后续按钮和分割线全部移除,按钮保存到数组中
  const newLength = cellElementList.length;
  const btnArr: Element[] = [];
  for (let i = 4; i < newLength; i++) {
    if (i % 2 === 0) {
      btnArr.push(cellElementList[4]);
    }
    cell.removeChild(cellElementList[4]);
  }
	// 2.创建更多下拉菜单
  const moreBtn = h(ElDropdown, null, {
    default: () =>
      h('span', { class: 'el-dropdown-link' }, [
        '更多',
        h(ElIcon, { class: 'el-icon--right', color: '#d9d9d9' }, () =>
          h(ArrowDown)
        ),
      ]),
    dropdown: () => h(ElDropdownMenu),
  });
  render(moreBtn, cell as HTMLElement);
  // 3.之前保存的按钮放到下拉菜单项中
  const dropdownMenuList: NodeList = document.querySelectorAll(
  	"div.el-dropdown__popper ul.el-dropdown-menu"
  );
  btnArr.forEach((btn) => {
    const li = document.createElement("li");
    li.setAttribute("data-el-collection-item", "");
    li.setAttribute("aria-disabled", "false");
    li.setAttribute("class", "el-dropdown-menu__item");
    li.setAttribute("tabindex", "-1");
    li.setAttribute("role", "menuitem");
    dropdownMenuList[dropdownMenuList.length - 1].appendChild(li);
    li.appendChild(btn);
  });
}

这里有几个值得注意的点需要说明一下:

  • 因为cellElementList是实时更新的,所以需要获取添加分割线后的新长度newLength,并且每次删除的元素始终是第4项
  • 这里的下拉菜单使用的是<el-dropdown>组件创建的虚拟DOM,关于创建虚拟DOM函数h()的语法可以参考下Vue的官网cn.vuejs.org/api/render-…
  • 由于h()只能创建虚拟DOM,所以不能在h(ElDropdownMenu)中传入保存的按钮真实DOMbtnArr,所以还是要使用JS创建下拉菜单项的DOM(不知道有没有真实DOM转虚拟DOM的方法~~~)
  • 下拉菜单项的DOM结构同样可以去官网查看,通过获取到所有行的下拉菜单项列表dropdownMenuList,然后逐行遍历挂载保存的按钮DOM,下图中的每个li的孩子就是需要挂载的按钮元素

dropdown-menu-dom.png

这里有个巨坑的点需要特别注意:**通过<el-dropdown>创建的每个下拉菜单,即上图中的DOM结构,会一直存在整个项目DOM的<body>节点下而不是这个页面的DOM节点下,且不随页面切换或组件卸载而消除。**这就会导致只要不刷新或者退出项目,切换页面后再切回来,之前创建的下拉菜单仍然存在,dropdownMenuList是一直增长的,所以每次遍历时不能从0开始,而是始终选中索引为dropdownMenuList.length - 1dropdownMenu节点。

5.其余调整

最后将其封装成一个函数updateButtonStyle,完整代码如下:

const updateButtonStyle = () => {
  const cellList: NodeList = document.querySelectorAll(
    "td.el-table-fixed-column--right > div.cell"
  );
  cellList.forEach((cell) => {
    const cellElementList: HTMLCollection = (cell as HTMLElement).children;
    const length = cellElementList.length;
    //  按钮个数大于等于2时,添加分割线
    if (length >= 2) {
      for (let i = 1; i < cellElementList.length; i += 2) {
        const divider = document.createElement("div");
        divider.setAttribute("class", "el-divider el-divider--vertical");
        divider.setAttribute("role", "separator");
        divider.setAttribute("style", "--el-border-style: solid;");
        cell.insertBefore(divider, cellElementList[i]);
      }
    }
    // 按钮个数大于等于4时,从第3个按钮开始将后续按钮放入更多下拉菜单中
    if (length >= 4) {
      //  1.将后续按钮和分割线全部移除,按钮保存到数组中
      const newLength = cellElementList.length;
      const btnArr: Element[] = [];
      for (let i = 4; i < newLength; i++) {
        if (i % 2 === 0) {
          btnArr.push(cellElementList[4]);
        }
        cell.removeChild(cellElementList[4]);
      }
      //  2.创建更多下拉菜单
      const moreBtn = h(ElDropdown, null, {
        default: () =>
          h("span", { class: "el-dropdown-link" }, [
            "更多",
            h(ElIcon, { class: "el-icon--right", color: "#d9d9d9" }, () =>
              h(ArrowDown)
            ),
          ]),
        dropdown: () => h(ElDropdownMenu),
      });
      render(moreBtn, cell as HTMLElement);
      // 3.之前保存的按钮放到下拉菜单项中
      const dropdownMenuList: NodeList = document.querySelectorAll(
        "div.el-dropdown__popper ul.el-dropdown-menu"
      );
      btnArr.forEach((btn) => {
        const li = document.createElement("li");
        li.setAttribute("data-el-collection-item", "");
        li.setAttribute("aria-disabled", "false");
        li.setAttribute("class", "el-dropdown-menu__item");
        li.setAttribute("tabindex", "-1");
        li.setAttribute("role", "menuitem");
        dropdownMenuList[dropdownMenuList.length - 1].appendChild(li);
        li.appendChild(btn);
      });
    }
  });
};

同时该函数需要在初始DOM渲染完成之后再做更改,所以需要在列表数据赋值后使用nextTick

setTimeout(() => {
  ...
  nextTick(updateButtonStyle);
}, 500);

此时修改后的页面如下:

逻辑完成表格.png

再调整一下样式即可:

.el-dropdown-link {
  cursor: pointer;
  color: var(--el-color-primary);
  height: 25px;
  display: flex;
  align-items: center;
}
.el-dropdown-link:focus {
	outline: none; 
}

最终渲染的页面如下:

最终表格样式.png