巧妙使用Vue.extend继承组件实现el-table双击可编辑(不使用v-if和v-else)

简介: 巧妙使用Vue.extend继承组件实现el-table双击可编辑(不使用v-if和v-else)

问题描述

有一个简单的表格,产品要求实现双击可编辑

看了一下网上的帖子,大多数都是搞两部分dom,一块是输入框,用于编辑状态填写另一块是普通标签,用于在不编辑显示状态下呈现单元格文字内容。再加上一个flag标识搭配v-if和v-else去控制编辑状态、还是显示状态。大致代码如下:

  <el-table-column
    align="center"
    label="姓名"
  >
    <template slot-scope="scope">
      <!--isClick就是标识状态,状态处于编辑时候,显示输入框,状态属于呈现状态就显示文本内容-->
      <el-input v-if="scope.row.isClick" v-model="scope.row.name"  @blur="blurFn(scope.row)"></el-input>
      <span @click="clickCell(scope.row)" v-else>{{scope.row.name}}</span>
    </template>
  </el-table-column>

这种方式有其适用场景,但是得每个el-table-column列中都加上el-input和span以及v-if和v-else。我们尝试一下动态添加el-input,就是点击那个单元格,给那个单元格添加el-input让其处于可编辑状态,然后适时移除即可。这样的话,很多列的时候,就不用加很多个v-if和v-else啦。我们先看一下效果图

效果图

1.gif

代码思路

  • 第1步:给el-table绑定双击事件 @cell-dblclick='dblclick',再双击事件的回调函数中,可以得知点击的是哪一行、那一列、那个单元格dom,以及点击事件。dblclick(row, column, cell, event) {...},这个是饿了么官方提供的,没啥好说的
  • 第2步:重点来喽

    • 第2.1步:单元格双击事件以后,我们首先创建一个el-input标签,然后把点击的这个单元格的值,作为参数props让这个el-input接收,这样的话el-input就会显示这个单元格的值了,就可以编辑了。问题一:如何创建一个el-input标签? ,客官稍等,下方会解答
    • 第2.2步:把创建好的el-input标签替换掉原来的单元格span标签,这样的话,就可以看到单元格变成了可输入的输入框了。问题二:如何把新创建的el-input标签,替换原有的span标签 ,客官稍等,下方会解答
    • 第2.3步,当用户编辑完了点击别处时候,即输入框失去焦点的时候,再把el-input输入框标签移除掉,恢复默认的span标签(当然失去焦点的时候,就要发请求修改数据了)问题三:如何移除el-input标签,并恢复原有的span标签,客官稍等,下方会解答
  • 这样的话,每次双击搞一个input标签用于修改,每次改完了失去焦点,就恢复默认单元格展示状态了,功能就实现了

代码思路中的三个问题解答

问题一:如何创建一个el-input标签?

我们知道,如果是创建原生的input标签并指定一个值,比较简单,直接:

let input = document.createElement('input') // 创建一个input标签
input.value = '孙悟空' // 给input标签赋值
document.body.appendChild(input) // 把input标签追加到文档body中

不过el-input标签不能通过上述方式创建,因为document.createElement()方法虽然可以创建出来el-input标签,但是dom并不认识这个el-input标签,所以页面没有变化。毕竟饿了么的el-input也是把input标签做一个二次封装的

所以,这里我们可以使用Vue.extend()方法去继承一个组件并暴露出去,而继承的这个组件中又有一个input标签,所以那个需要使用,那里就可以引入并new出来一个el-input了。关于Vue.extend()的定义啥的,这里不赘述,详情看官方文档。笔者之前也写过一篇Vue.extend文章,传送门:https://juejin.cn/post/7021724333391216677

首先搞一个.vue文件,用于继承

// input.vue文件
<template>
  <div class="cell">
    <el-input
      ref="elInputRef"
      size="mini"
      v-model.trim="cellValue"
    ></el-input>
  </div>
</template>

props: {
    cellValue: {
      type: String | Number,
      default: "",
    },
}

然后定义一个data.js文件,继承input.vue文件,并暴露

// data.js
import Vue from "vue";
import definedInput from "./input.vue";
// vue继承这个input组件,就相当于一个构造函数了
const inputC = Vue.extend(definedInput);
// 暴露出去,哪里需要哪里引入
export default {
    inputC,
}

页面中引入并使用

// page.vue
import extendComponents from "./threeC/data"; // 1. 引入

new extendComponents.inputC({ // 2. 实例化
    propsData: {
      // 使用propsData对象传递参数,子组件在props中可以接收到
      cellValue: cellValue, // 传递单元格的值
    },
  }).$mount(cell.children[0]);// 3. 挂载
propsData对象用于给继承的组件传递参数,也可以传递一个函数,从而继承组件通过这个函数通知外部使用组件,详情见后续完整代码

问题二三:el-input标签和span标签的来回替换恢复

使用$mount方法去做来回替换,$mount可以把一个子dom元素追加到父dom元素内部,相当于appendChild

然后这里需要有一个替换的时机,就是实例化的组件中的el-input失去焦点的时候,去通知外部使用的组件,所以可以在外部使用是,在propsData中传递一个函数到继承的组件,如:

// 外部组件传递
new extendComponents.inputC({
    propsData: {
      cellValue: cellValue, // 传递单元格的值
      saveRowData: this.saveRowData, // 传递回调函数用于通知,继承组件中可以触发之
    },
}).$mount(cell.children[0]); 

saveRowData(params){
    console.log('收到继承组件消息通知啦参数为:',params)
}
// 内部组件失去焦点时候通知
<el-input
  ref="elInputRef"
  size="mini"
  v-model.trim="cellValue"
  @blur="blurFn"
></el-input>

props: {
    cellValue: {
      type: String | Number,
      default: "",
    },
    saveRowData: Function, // 外部,传递进来一个函数,当这个el-input失去焦点的时候,通过此函数通知外部
}

blurFn() {
  // 失去焦点,再抛出去,通知外部
  this.saveRowData({
    cellValue: this.cellValue,
    // 其他参数
  });
},

所以当内层失去焦点的时候,就可以通知外层去做一个替换了,就是把单元格dom重新做一个$mount挂载,就把el-input替换成了span了,为了进一步理解,这里的span我们也可以使用继承的方式,是new实例化使用,详情见下方完整代码

完整代码

目录结构

threeC
-- data.js
-- input.vue
-- span.vue
three.vue

用于继承的el-input组件

input.vue

<template>
  <div class="cell">
    <el-input
      ref="elInputRef"
      size="mini"
      v-model.trim="cellValue"
      @blur="blurFn"
    ></el-input>
  </div>
</template>

<script>
export default {
  props: {
    cellValue: {
      type: String | Number,
      default: "",
    },
    saveRowData: Function, // 外部,传递进来一个函数,当这个el-input失去焦点的时候,通过此函数通知外部
    cellDom: Node, // 单元格dom
    row: Object, // 单元格所在行数据
    property: String, // 单元格的key
  },
  mounted() {
    // 用户双击后,让其处于获取焦点的状态
    this.$refs.elInputRef.focus();
  },
  methods: {
    blurFn() {
      // 失去焦点,再抛出去,通知外部
      this.saveRowData({
        cellValue: this.cellValue,
        cellDom: this.cellDom,
        row: this.row,
        property: this.property,
      });
    },
  },
};
</script>

<style>
.cell {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  box-sizing: border-box;
  padding: 0 8px;
}
</style>

用于继承的span组件

span.vue

<template>
  <span class="cell">{{ cellValue }}</span>
</template>

<script>
export default {
  props: {
    cellValue: {
      type: String | Number,
      default: "",
    },
  },
};
</script>

统一继承并暴露data.js文件

import Vue from "vue";
import definedInput from "./input.vue";
import definedSpan from "./span.vue";

const inputC = Vue.extend(definedInput);
const spanC = Vue.extend(definedSpan);

export default {
    inputC,
    spanC,
}

使用继承的three.vue组件

<template>
  <div id="app">
    <el-table
      @cell-dblclick="dblclick"
      :cell-class-name="cellClassName"
      height="480"
      :data="tableData"
      border
    >
      <el-table-column align="center" type="index" label="序号" width="50">
      </el-table-column>
      <el-table-column align="center" prop="name" label="姓名" width="100">
      </el-table-column>
      <el-table-column align="center" prop="age" label="年龄" width="100">
      </el-table-column>
      <el-table-column align="center" prop="home" label="家乡">
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
// 引入继承组件对象,可取其身上的inputC构造函数、或spanC构造函数生成组件dom
import extendComponents from "./threeC/data";
export default {
  data() {
    return {
      tableData: [
        {
          name: "孙悟空",
          age: 500,
          home: "花果山水帘洞",
        },
        {
          name: "猪八戒",
          age: 88,
          home: "高老庄",
        },
        {
          name: "沙和尚",
          age: 1000,
          home: "通天河",
        },
      ],
      /**
       * 存一份旧的值,用于校验是否发生变化,是否修改
       * */
      oldCellValue: null,
    };
  },
  methods: {
    cellClassName({ row, column, rowIndex, columnIndex }) {
      row.index = rowIndex; // 自定义指定一个索引,下方能够用到
    },
    dblclick(row, column, cell, event) {
      // 1. 序号列单元格不允许编辑,别的列单元格可以编辑
      if (column.label == "序号") {
        this.$message({
          type: "warning",
          message: "序号列不允许编辑",
        });
        return;
      }
      // 2. 存一份旧的单元格的值
      this.oldCellValue = row[column.property];
      // 3. 然后把单元格的值,作为参数传递给实例化的input组件
      let cellValue = row[column.property];
      // 4. 实例化组件以后,带着参数,再挂载到对应位置
      new extendComponents.inputC({
        propsData: {
          // 使用propsData对象传递参数,子组件在props中可以接收到
          cellValue: cellValue, // 传递单元格的值
          saveRowData: this.saveRowData, // 传递回调函数用于保存行数据,组件中可以触发之
          cellDom: cell, // 传递这个dom元素
          row: row, // 传递双击的行的数据
          property: column.property, // 传递双击的是哪个字段
        },
      }).$mount(cell.children[0]); // 5. $mount方法,用于将某个dom挂载到某个dom上
    },
    /**
     * 失去焦点的时候有以下操作
     *    1. 校验新值是否等于原有值,若等于,说明用户未修改,就不发请求。若不等于就发请求,然后更新tableData数据
     *    2. 然后使用$mount方法,挂载一个新的span标签dom在页面上,即恢复原样,而span标签也是实例化的哦
     * */
    saveRowData(params) {
      console.log("继承的子组件传递过来的数据", params);
      // 1. 看看用户是否修改了
      if (params.cellValue == this.oldCellValue) {
        console.log("未修改数据,不用发请求");
      } else {
        params.row[params.property] = params.cellValue;
        // 这里模拟一下发了请求,得到最新表体数据以后,更新tableData
        setTimeout(() => {
          //        给那个数组的     第几项            修改为什么值
          this.$set(this.tableData, params.row.index, params.row);
        }, 300);
      }
      // 2. 恢复dom节点成为原来的样子,有下面两种方式

      /**
       * 方式一:使用官方推荐的$mount去挂载到某个节点上,上方也是
       * */
      new extendComponents.spanC({
        propsData: {
          cellValue: params.cellValue,
        },
      }).$mount(params.cellDom.children[0]);

      /**
       * 方式二:使用原生js去清空原节点内容,同时再添加子元素
       * */
      // let span = document.createElement("span"); // 创建一个span标签
      // span.innerHTML = params.cellValue; // 指定span标签的内容的值
      // span.classList.add("cell"); // 给span标签添加class为cell
      // params.cellDom.innerHTML = ""; // 清空刚操作的input标签的内容
      // params.cellDom.appendChild(span); // 再把span标签给追加上去,恢复原样
    },
  },
};
</script>

<style lang="less" scoped>
#app {
  width: 100%;
  height: 100vh;
  box-sizing: border-box;
  padding: 50px;
}
</style>

总结

使用Vue.extend()方法,可以继承一些组件,甚至继承一些复杂的组件,在实际业务场景中会有巧妙的使用。具体业务场景具体分析。

此外,上述代码中是el-input的继承,其实,我们也可以做el-select的继承,思路和上方类似,这样就可以在表格中双击单元格,选择并更改对应的下拉框更改el-table的单元值了,比如如果有性别这一列,那是下拉框的形式的。道友们可以按照这个思路发散哦...

好记性不如烂笔头,记录一下吧 ^_^

2022-08-25补充~单元格校验功能

首先感谢大家的点赞鼓励,笔者会继续加油的,争取少写一点水文(手动狗头)。有道友在评论区说,要加上校验功能,这里笔者提供了一种思路:

思路分析

  1. 指定校验函数和校验函数未通过的提示文字
  2. 然后将其作为参数传递到需要实例化的el-input上
  3. 需要实例化的el-input再使用传递进来的校验函数再传递出去
  4. 根据校验函数的结果是否通过,做相应处理

假设表格有如下校验规则

  • 姓名列单元格不能超过6个字
  • 年龄列单元格不能大于1000岁(且为数字类型)
  • 家乡列单元格不做限制

效果图如下:

jiaoyan.gif

主要更改的代码:

// 校验函数,比如姓名列单元格最多只能输入6个字符
function nameMaxLength6(str) {
  return str.length <= 6;
}
// 校验函数,比如年龄列单元格值最大为1000岁
function ageMaxValue1000(str) {
  return Number(str) <= 1000;
}
// 家乡列单元格不做校验处理,始终为true(如果别的列也不需要校验,也直接使用这个函数即可)
function noValidate(params) {
  return true;
}

tableData: [ ... ],
// 存一份旧的值,校验未通过时还是使用旧值
oldCellValue: null,
// 新加的校验规则,配置传参匹配方式
columnAddValidate: {
    name: {
      validateFn: nameMaxLength6,
      validateMessage: "名字最大不能超过6个字符",
    },
    age: {
      validateFn: ageMaxValue1000,
      validateMessage: "注意!年龄是数字类型,且最大不能超过1000岁",
    },
    home: {
      validateFn: noValidate, // validateFn参数必填哦
    },
},

// 4. 实例化组件以后,带着参数,再挂载到对应位置
new extendComponents.inputC({
    propsData: {
      // 使用propsData对象传递参数,子组件在props中可以接收到
      cellValue: cellValue, // 传递单元格的值
      saveRowData: this.saveRowData, // 传递回调函数用于保存行数据,组件中可以触发之
      cellDom: cell, // 传递这个dom元素
      row: row, // 传递双击的行的数据
      property: column.property, // 传递双击的是哪个字段
      /**
       * 新加的动态校验函数
       *      只需要指定校验的函数,以及校验未通过时候的文字提示(对象匹配方式)
       * */
      validateFn: this.columnAddValidate[column.property].validateFn,
      validateMessage:
        this.columnAddValidate[column.property].validateMessage,
    },
}).$mount(cell.children[0]); // 5. $mount方法,用于将某个dom挂载到某个dom上

blurFn() {
  // 失去焦点,再抛出去,通知外部
  this.saveRowData({
    cellValue: this.cellValue,
    cellDom: this.cellDom,
    row: this.row,
    property: this.property,
    // 加上校验函数,传递校验函数就使用校验函数校验一下,没传递校验函数就默认为true通过校验
    isPassValidate: this.validateFn(this.cellValue),
    validateMessage: this.validateMessage,
  });
},

......

完整代码地址

个人建议,直接去笔者的github仓库拉下来完整代码,跑起来看一下,更加有助于理解。仓库地址:https://github.com/shuirongshuifu/elementSrcCodeStudy/blob/main/src/views/otherViews/clickTableCanEdit.vue

另外本仓库是笔者仿写饿了么UI组件的代码仓库,如果也能帮到您,欢迎大手一挥给个star哦^_^
相关文章
|
25天前
|
JavaScript
在 Vue 中处理组件选项与 Mixin 选项冲突的详细解决方案
【10月更文挑战第18天】通过以上的分析和探讨,相信你对在 Vue 中使用 Mixin 时遇到组件选项与 Mixin 选项冲突的解决方法有了更深入的理解。在实际开发中,要根据具体情况灵活选择合适的解决方案,以确保代码的质量和可维护性。
80 7
|
7天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
7天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
23天前
|
缓存 JavaScript UED
Vue 的动态组件与 keep-alive
【10月更文挑战第19天】总的来说,动态组件和 `keep-alive` 是 Vue.js 中非常实用的特性,它们为我们提供了更灵活和高效的组件管理方式,使我们能够更好地构建复杂的应用界面。深入理解和掌握它们,以便在实际开发中能够充分发挥它们的优势,提升我们的开发效率和应用性能。
44 18
|
18天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
23天前
|
前端开发 UED
vue3知识点:Suspense组件
vue3知识点:Suspense组件
30 4
|
22天前
|
JavaScript 前端开发 测试技术
组件化开发:创建可重用的Vue组件
【10月更文挑战第21天】组件化开发:创建可重用的Vue组件
24 1
|
23天前
|
JavaScript 前端开发 Java
《vue3第五章》新的组件,包含:Fragment、Teleport、Suspense
《vue3第五章》新的组件,包含:Fragment、Teleport、Suspense
32 2
|
23天前
|
Java
vue3知识点:Teleport组件
vue3知识点:Teleport组件
25 1
|
25天前
|
存储 JavaScript
Vue 组件间通信的方式有哪些?
Vue组件间通信主要通过Props、Events、Provide/Inject、Vuex(状态管理)、Ref、Event Bus等实现,支持父子组件及跨级组件间的高效数据传递与状态共享。