likes
comments
collection
share

基于AntdV封装一个点击行即可选中当前行的Table

作者站长头像
站长
· 阅读数 23

前言

最近公司有个需求,需要将老项目中所有可以选择的表格都变成点击每一行数据就可以选中当前行,这样表格行数据过长时就不用回到头部点击复选框进行选择。

需求分析

既然是将老项目中所有的可选表格都添加这个功能,那么封装一个新的组件是很有必要的。这样以后还需要这类功能的表格就可以直接使用新的组件了。

因为是老项目,vue版本和组件库版本比较老,所以本文中使用到的API都是比较旧的,如果你使用的组件库版本或者vue版本和本文的不同,还是需要改吧改吧的。不过老话说的好,只要思想不滑坡,方法总比困难多。

  • vue:2.6.11
  • ant-design-vue:1.4.12

思路

  1. 查看antdv文档看看有无对应参数支持,通过UI库自带方案解决。
  2. 通过DOM操作获取到目标表格,然后给表格tbody中每个tr都绑定点击事件(下策)。

有了思路就立马开干!!

方案1

通过查询相关文档发现,antd官方虽然没有直接参数支持这类功能,但是他们提供了一个customRow参数,该参数接受一个回调函数,回调函数的第一个参数是每行的数据,允许用户自定义给每行添加一些事件或者属性。

基于AntdV封装一个点击行即可选中当前行的Table

那么问题来了,虽然官方提供这么一个参数让我们给表格行绑定事件方便了许多,但是该如何做到让以前的旧代码改动最小的情况下添加上这个功能呢?这里我决定采用$attrs$listeners来最大限度降低原代码的改动。(如果是使用VUE3.x的话只用绑定$attrs就可以了,因为VUE3.x后$listeners已经没有了,被融合进$attrs了)

<template lang="pug">
  a-table(
  v-bind="$attrs",
  v-on="$listeners",
  )
</template>

因为customRow是在$attrs中的,所以我们这里还不能直接绑定$attrs,需要对它进行一些处理。

这里需要思考几个点:

  1. 如何获取selectedRowKeys数组?
  2. 修改selectedRowKeys时如何通知父组件更新?
  3. 如果在父组件中已经使用过customRow该怎么办?

ok,接下来让我们一步步解决这些问题。

如何获取selectedRowKeys数组?

这个问题很简单,我们可以通过解构$attrs拿到对应的rowSelection,再通过rowSelection拿到selectedRowKeys

修改selectedRowKeys时如何通知父组件更新?

这个问题一开始我想到的是通过emit通知父组件更新selectedRowKeys,但是如果通过emit来通知的话,那每次使用这个组件的时候还要声明一个处理函数来接收,很麻烦。(注意:这里通过$attrs.rowSelection.selectedRowKeys直接赋值是不生效的哦,并且这样做也是不提倡的)。

经过一段时间的苦思冥想,终于想到一个不错的解决方案。

既然这个表格是一个可选表格,那么它一定会传入对应的onChange处理函数,而我们又可以通过 $attrs拿到onChange处理函数,我们直接通过手动执行onChange处理函数不就可以让父组件自动更新selectedRowKeys的值了。当然,这里还需要考虑传递给onChange的参数(如selectedRows等)以及type==='radio' 的情况。

{
   transClick (record, originHandler, event) {
     const {
       selectedRowKeys,
       onChange,
       onSelect,
       type
     } = this.otherProps.rowSelection

     let updatedSelectedRowKeys, selected, index
     if (type === 'radio') {
       updatedSelectedRowKeys = [record.id]
       this.selectedRows = [record]
       selected = true
     } else {
       // 返回更新后的rowKeys数组
       ;[updatedSelectedRowKeys, selected, index] = this.rowSelect(
         selectedRowKeys || [],
         record[this.keyName]
       )
       selected
         ? this.selectedRows.push(record)
         : this.selectedRows.splice(index, 1)
     }
     this.toUpdate(
       { onChange, onSelect },
       {
         updatedSelectedRowKeys,
         selectedRows: this.selectedRows,
         selected,
         record
       }
     )
     originHandler && originHandler(event)
   },
  toUpdate (updateHandler, updateData) {
    const { onChange, onSelect } = updateHandler
    const {
      updatedSelectedRowKeys,
      selectedRows,
      selected,
      record
    } = updateData
    onChange && onChange(updatedSelectedRowKeys, selectedRows)
    onSelect && onSelect(record, selected, selectedRows)
  }
  }

如果在父组件中已经使用过customRow该怎么办?

如果在父组件中已经使用过customRow,那我们需要先将customRow取出来,再执行customRow函数得到返回的属性对象,通过属性对象拿到对应的click处理函数,将处理函数放入我们的代码中再返回一个新的customRow函数给到Table组件。

注意:如果使用的antdv版本比较新,那么返回的属性对象需要修改一下。

transformCustomRow (configFn = () => ({})) {
  		// 如果用户没有传 customRow 属性,那么configFn默认是一个函数
      return (record) => {
        const { on, ...other } = configFn(record)
        let c, h
        if (on && on.click) {
          const { click, ...handles } = on
          c = click
          h = handles
        }
        return {
          on: {
            click: this.transClick.bind(this, record, c),
            ...h
          },
          ...other
        }
      }
    }

方案2

既然方案1走通了,那么方案2这种通过操作频繁操作DOM来添加事件的方案就没有必要写了,一方面是性能不如方案1,另一方面更容易在封装的时候出错。

组件完整代码

注意:组件中因为onSelect是手动调用的,所以无法传递对应的事件对象,如果在父组件中传递的onSelect处理函数中需要使用Event对象,那可能这里就不适合使用这个组件了。

<template>
<a-table v-bind="otherProps" v-on="$listeners"/>
</template>
<script>
export default {
  name: 'RowClickTable',
  props: {
    // rowkey在每行行数据中对应的键名
    keyName: {
      type: String,
      default: 'id'
    }
  },
  data () {
    return {
      otherProps: null,
      selectedRows: []
    }
  },
  computed: {
    selectedRowKeys () {
      // 防止父组件中不传递selectedRowKeys
      return this.selectedRows.map(item => item[this.keyName])
    }
  },
  methods: {
    rowSelect (selectedArr, data, prop) {
      let copyArr = [...selectedArr]
      const index = copyArr.findIndex(item =>
        prop ? item[prop] === data[prop] : item === data
      )
      const selected = index !== -1
      if (selected) {
        // 已选中的数组中已经含有当前data
        copyArr.splice(index, 1)
      } else {
        copyArr.push(data)
      }
      // selected是被加入进数组还是移除的标识
      return [copyArr, !selected, index]
    },
    transClick (record, originHandler, event) {
      const {
        selectedRowKeys,
        onChange,
        onSelect,
        type
      } = this.otherProps.rowSelection

      let updatedSelectedRowKeys, selected, index
      if (type === 'radio') {
        updatedSelectedRowKeys = [record.id]
        this.selectedRows = [record]
        selected = true
      } else {
        // 返回更新后的rowKeys数组
        [updatedSelectedRowKeys, selected, index] = this.rowSelect(
          selectedRowKeys || this.selectedRowKeys,
          record[this.keyName]
        )
        selected
          ? this.selectedRows.push(record)
          : this.selectedRows.splice(index, 1)
      }
      this.toUpdate(
        { onChange, onSelect },
        {
          updatedSelectedRowKeys,
          selectedRows: this.selectedRows,
          selected,
          record
        }
      )
      originHandler && originHandler(event)
    },
    toUpdate (updateHandler, updateData) {
      const { onChange, onSelect } = updateHandler
      const {
        updatedSelectedRowKeys,
        selectedRows,
        selected,
        record
      } = updateData
      onChange && onChange(updatedSelectedRowKeys, selectedRows)
      onSelect && onSelect(record, selected, selectedRows)
    },
    transformCustomRow (configFn = () => ({})) {
      return record => {
        const { on, ...other } = configFn(record)
        let c, h
        if (on && on.click) {
          const { click, ...handles } = on
          c = click
          h = handles
        }
        return {
          on: {
            click: this.transClick.bind(this, record, c),
            ...h
          },
          ...other
        }
      }
    }
  },
  watch: {
    $attrs: {
      handler (value) {
        const cValue = { ...value }
        const { customRow } = cValue

        cValue.customRow = this.transformCustomRow(customRow)
        this.otherProps = cValue
      },
      deep: true,
      immediate: true
    }
  }
}
</script>
<style lang="less" scoped></style>

封装好这个组件,这样在修改老代码的时候就只用修改一下组件名,不用担心会影响到之前业务的功能了。😊