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

2022-10-14

这篇文章主要给大家介绍了关于如何巧用Vue.extend继承组件实现el-table双击可编辑的相关资料,不使用v-if、v-else,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

目录
  • 问题描述
  • 效果图
  • 代码思路
  • 代码思路中的三个问题解答
    • 问题一:如何创建一个el-input标签?
    • 问题二三:el-input标签和span标签的来回替换恢复
  • 完整代码
    • 目录结构
    • 用于继承的el-input组件
    • 用于继承的span组件
    • 统一继承并暴露data.js文件
    • 使用继承的three.vue组件
  • 总结

    问题描述

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

    看了一下网上的帖子,大多数都是搞两部分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步:给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://www.jb51.net/article/251484.htm

    首先搞一个.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的单元值了,比如如果有性别这一列,那是下拉框的形式的。道友们可以按照这个思路发散哦...

    到此这篇关于如何巧用Vue.extend继承组件实现el-table双击可编辑(不使用v-if、v-else)的文章就介绍到这了,更多相关Vue.extend实现el-table双击可编辑内容请搜索北冥有鱼以前的文章或继续浏览下面的相关文章希望大家以后多多支持北冥有鱼!

    您可能感兴趣的文章:

    • Vue.extend实现组件库message组件示例详解
    • vue使用Vue.extend方法仿写个loading加载中效果实例
    • 通过vue.extend实现消息提示弹框的方法记录
    • Vue.extend 登录注册模态框的实现
    • Vue.extend 编程式插入组件的实现
    • Vue中Vue.extend()的使用及解析

    《如何巧用Vue.extend继承组件实现el-table双击可编辑(不使用v-if、v-else).doc》

    下载本文的Word格式文档,以方便收藏与打印。