造轮子-table组件的实现

我们需要实现的功能
  1. 展示数据(带边框,紧凑型/松散型,斑马条纹)
  2. 选中数据(单选/全选)
  3. 展示排序
  4. 固定表头/列
  5. 可展开
组件的基本结构
  • table.vue



  • demo.vue

data() {
            return {         
                columns: [
                    //表头每一列显示的文本和字段
                    {text: '姓名', field: 'name'},
                    {text: '分数', field: 'score'}
                ],
                dataSource: [
                    {id: 1, name: '发发', score: 100},
                    {id: 2, name: '琳琳', score: 99}
                ]
            }
        },
实现checkbox全选和单选

思路: 通过单向数据流,外界好传入一个selectedItem,table接受这个selectedItem,默认为空数组
单选:对表单内容的checkbox添加change事件onChangeItem把每一列行的index,item还有原生event都传进去,然后当改变checkbox状态的时候,先对selectedItem数组进行深拷贝,然后如果选中状态就把item push到深拷贝后的数组,如果没选中就从数组中找到当前item,删除,最后触发一个updae:selectedItem事件,把拷贝后的数组传进去 多选:给表头添加change事件onChangeItemAll传入一个原生event,如果是选中状态就把传入的所有数据的数组dataSource作为参数通过emit传给updae:selectedItem事件,没选中就把空数组传进去,然后在表单内容里的checkbox标签中每一个都加一个checked变量,通过判断selectedItem数组中是否包含当前对应的列来更改选中状态 半选:通过给全选的checkbox加一个ref=refs.a.indeterminate = true,其他情况都等于false

  • demo.vue
  • table.vue





解决取消选中异常的bug
当我们全选的时候,比如我点击第二个取消选中可最后一个也跟着取消选中了,原因就是因为深拷贝的原因,因为深拷贝后数组里的对象和原先的不是同一个索引,所以不能通过indexOf查找,而要通过取消选中的时候,当前取消选中项的id,找到深拷贝后数组里的id不等于当前选中项的id的元素,然后传给父组件

    let copy = JSON.parse(JSON.stringify(this.selectedItem))
                if(e.target.checked){
                    copy.push(item)
                }else{
                    //取消选中状态:点击当前的checkbox保留数组中id不等于当前id的项
                    copy= copy.filter(i=>i.id !== item.id)
                }
                this.$emit('update:selectedItem',copy)
            },
watch: {
    selectedItem(){
        if(this.selectedItem.length === this.dataSource.length){
            this.$refs.a.indeterminate = false  
        }else if(this.selectedItem.length === 0){
            this.$refs.a.indeterminate = false 
        }else{
            this.$refs.a.indeterminate = true
        }
    }
}

实现选中所有单选的checkbox后,全选的checkbox也跟着选中
错误写法:
直接判断selectedItem.length和dataSource.length的长度是否相等,这样虽然可以实现我们要的功能,但是是错误的写法


computed: {
  areAllItemChecked(){
  return this.selectedItem.length === this.dataSource
}
}

正确写法:
我们需要判断这两个数组里的所有项的id是否都相等来判断是否全选。
那么现在的问题是我们如何判断两个数组是一样的。比如this.dataSource=[{id:1},{id:2}]this.selectedItem = [{id:2},{id:1}]我们该如何判断两个数组是一样的哪?
我们无法通过遍历判断第一个dataSource的第一个是否等于selectedItem的第一个来判断是否相等,因为上面的顺序不一样,但也是相等的。
所以我们需要

  1. 先对这两个数组里的id进行排序
this.dataSource.sort((a,b)=>a.id - b.id)

但是sort会改变原来的数组,所以我们需要先用map生成一个新的数组,然后在排序

areAllItemChecked(){
  const a = this.dataSource.map(n=>n.id).sort()
  const b = this.selectedItem.map(n=>n.id).sort()
  if(a.length === b.length){
      for(let i = 0;i
表格排序的实现

补充知识:在vue里,你不可能根据一个属性检查另一个属性的合法性,因为你在validate中拿不到实例(this)

export default{
  props: {
    name: 'lifa'
  },
  orderBy: {
    type: Object,
    default: ()=>({}),
    validator(object){
      console.log(this)//undefined
    }
  }
  
}

定义排序规则:
通过外界传入一个ordreBy对象来定义

orderBy: {//true:开启排序,但是不确定asc desc
    name: true,
    score: 'desc'
},

如果想要某个列按升序排列就在当前字段后加一个'asc',降序就加'score',默认开启排序,但是不是升序也不是降序就用true,然后在组件中接受这个orderBy


    
{{column.text}}
props: { //通过什么排序 orderBy: { type: Object, default: ()=>({}) } }, methods: { changeOrderBy(key){ const copy = JSON.parse(JSON.stringify(this.orderBy)) if(copy[key] === 'asc'){ copy[key] = 'desc' }else if(copy[key] === 'desc'){ copy[key] = true }else{ copy[key] = 'asc' } this.$emit('update:orderBy',copy) } }

上面的代码可以实现状态的切换,当点击的时候如果是升序就会变成降序,如果是降序就会变成默认状态,否则就会变成升序,然后需要在外界.sync一下


然后我们只需要监听这个update:orderBy事件,当事件触发的时候执行一个方法,来修改我们dataSource里的数据


methods: {
            changeOrder(data){
                this.$nextTick(()=>{
                    let type
                    let arr = this.dataSource.map(item=>{
                        type = typeof item[this.key]
                        return item[this.key]
                    })
                    if( data[this.key]=== 'asc'){
                        if(type === 'number'){
                            arr.sort((a,b)=>a-b)
                        }else{
                            arr.sort((a, b) => b.localeCompare(a, 'zh-Hans-CN', {sensitivity: 'accent'}))
                        }
                    }else if(data[this.key] === 'desc'){
                        if(type === 'number'){
                            arr.sort((a,b)=>b-a)
                        }else{
                            arr.sort((a, b) => a.localeCompare(b, 'zh-Hans-CN', {sensitivity: 'accent'}))
                        }
                    }
                    arr.map((item,index)=>{
                        this.dataSource[index][this.key]=item
                    })

                })
            },
        },
        watch: {
        //监听orderBy的变化,如果新值里的属性值不等于旧值的属性值说明当前属性变了,拿到这个key,比如我一开始是{name: '发发',score:100}现在是{name: '发发',score:90},那么我们就可以拿到score这个key
          orderBy(val,oldVal){
              for(let key in this.orderBy){
                  if(val[key] !== oldVal[key]){
                      this.key = key
                  }
              }
          }
        },
实现表头固定

思路:

  1. 对table拷贝一份,然后把拷贝后的table中的tbody删除,只留一个thead固定在顶部(这里因为thead必须在table中,所以我们不能像div一样把它单独拿出来)
  2. 复制后的table删除tbody后,宽度会和之前的不一致,这时候你需要获取拷贝前的table里每一个th的宽度,然后设置给拷贝后的th
  • table.vue

props: {
  height: {
    type: [Number, String],
  }
},
mounted(){
  let oldTable = this.$refs.table
let newTable = oldTable.cloneNode(true)
this.newTable = newTable
this.updateHeadersWidth()
//窗口改变时重新获取一下各个th的宽度,重新赋值
window.addEventListener('resize', this.onWindowResize)
newTable.classList.add('lifa-table-copy')
this.$refs.warpper.appendChild(newTable)
},
beforeDestroy() {
            window.removeEventListener('resize', this.onWindowResize)
            this.newTable.remove()
        },
methods: {
  onWindowResize() {
                this.updateHeadersWidth()
            },
            updateHeadersWidth() {
                let tableHeader = Array.from(this.$refs.table.children).filter(node => node.nodeName.toLocaleLowerCase() === 'thead')[0]
                let tableHeader2
                Array.from(this.newTable.children).map((node) => {
                    if (node.nodeName.toLocaleLowerCase() === 'tbody') {
                        node.remove()
                    } else {
                        //也就是tableHeader=
                        tableHeader2 = node
                    }
                })
                Array.from(tableHeader.children[0].children).map((node, index) => {
                    let {width} = node.getBoundingClientRect()
                    console.log(width);
                    tableHeader2.children[0].children[index].style.width = `${width}px`
                })
            },
}

上面的代码虽然实现了顶部表头固定,但是因为你是复制了一个,所以表头的点击事件并不会起作用。
解决方法:
我们在拷贝一个新的table的时候不全部拷贝只拷贝table不拷贝table里面的子元素,然后我们把原有的thead添加到拷贝后的table中,这样就还是原来的thead,而且可以单独拿出来,然后我们通过让用户给每一列传一个宽度,通过这个宽度来设置原来的table里的宽度,并且拷贝后的table因为会覆盖表格内容的第一行,所以还要通过margin-top移下来thead的高度的距离

data(){
  return {
    columns: [
                    //表头每一列显示的文本和字段
                    {text: '姓名', field: 'name', width: 100},
                    {text: '分数', field: 'score',width: 100},
                    {text: '年龄', field: 'age'}
                ],
                orderBy: {//true:开启排序,但是不确定asc desc
                    name: true,
                    score: 'desc'
                },
                dataSource: [
                    {id: 1, name: '发发', score: 100, age:18},
                    {id: 2, name: '琳琳', score: 99, age: 16},
                    {id: 3, name: '西西', score: 99, age: 20},
                    {id: 4, name: '泳儿', score: 99, age: 21},
                    {id: 5, name: '美美', score: 99, age: 22},
                    {id: 6, name: '阿宇', score: 99, age: 26},
                    {id: 7, name: '发发', score: 100, age:18},
                    {id: 8, name: '琳琳', score: 99, age: 16},
                    {id: 9, name: '西西', score: 99, age: 20},
                    {id: 10, name: '泳儿', score: 99, age: 21},
                    {id: 11, name: '美美', score: 99, age:18},
                    {id: 12, name: '阿宇', score: 99, age: 16}
                ],
  }
}
  • table.vue

mounted() {
    let oldTable = this.$refs.table
    let newTable = oldTable.cloneNode()
    let {height} = oldTable.children[0].getBoundingClientRect()
    newTable.appendChild(oldTable.children[0])
    this.$refs.tableContent.style.marginTop = height + 'px'
    this.$refs.wrapper.style.height = this.height - height + 'px'
    newTable.classList.add('lifa-table-copy')
    this.$refs.wrapper.appendChild(newTable)
},
实现展开行功能

实现思路:通过用户在dataSource里传入description指定展开要显示的字段,然后通过一个expend-field传入我们的description字段,也就是expend-field="description"。因为我们要在每一个tr下面再加一个tr,但是你不能再单独v-for那就不能同步了,所以要想同时遍历多个外层就要用template,然后给每一列前面加一个展开的按钮,点击这个按钮的时候把当前的id传给一个数组,然后通过判断数组里是否有这个id,没有就加到数组里,有就删除,之后再给展开的这一栏通过判断数组里是否有当前id来让它是否展示

  • table.vue

methods: {
  expendItem(id){
                if(this.expendIds.indexOf(id) >= 0){
                    this.expendIds.splice(this.expendIds.indexOf(id),1)
                }else{
                    this.expendIds.push(id)
                }
            },
            expendVisible(id){
                return this.expendIds.indexOf(id) >= 0
            },
}

dataSource: [
                    {id: 1, name: '发发', score: 100, age:18, description: '你最帅'},
                    {id: 2, name: '琳琳', score: 99, age: 16, description: '为啥不做我媳妇'},
                    {id: 3, name: '西西', score: 99, age: 20, description: '好累啊'},
                    {id: 4, name: '泳儿', score: 99, age: 21},
                    {id: 5, name: '美美', score: 99, age: 22},
                    {id: 6, name: '阿宇', score: 99, age: 26},
                    {id: 7, name: '发发', score: 100, age:18},
                    {id: 8, name: '琳琳', score: 99, age: 16},
                    {id: 9, name: '西西', score: 99, age: 20},
                    {id: 10, name: '泳儿', score: 99, age: 21},
                    {id: 11, name: '美美', score: 99, age:18},
                    {id: 12, name: '阿宇', score: 99, age: 16}
                ],

上面的展开行里的列数是写死的,而我们有可能没有展开按钮或者没有选择框,那么这个列数就不对了,所以我们需要通过计算属性来计算


                        
                            {{item[expendField] || '空'}}
                        
                    
computed: {
  expendedCellColSpan(){
    let result = 0
    if(this.checkable){
        result += 1
    }
    if(this.expendField){
        result += 1
    }
    return result
}
},
props: {
  //是否显示选择框
            checkable: {
                type: Boolean,
                default: false
            }
}

table里面可以有按钮

思路通过用户传入一个template,然后在table中通过slot来把你template里的按钮放到指定位置


                
            
  • table.vue

    

methods: {
  edit(id){
                alert(`正在编辑第${id}个`)
            },
            view(id){
                alert(`正在查看第${id}个`)
            }
}
升级table组件,支持自定义

问题:我们table里只能传入data而不能传入a标签,我们现在想给每一列添加一个a标签,使用vue就没法实现
思路:使用jsx

  1. 在vue中使用JSX
    (1).lang="jsx"
    (2).render返回一个标签
    (3).使用css直接还是用class



问题:我们必须得将之前的代码都改成jsx,而且使用的人也必须使用jsx格式,限制性太多

  1. 使用slot插槽自定义
    相关补充:我们可以在组件里写任何组件标签,这些组件标签会默认转为插槽,获取他们的方式有两种,一:在mounted中通过this.children拿到他们

将原来的columns数组去掉,使用组件table-columns来代替

  • table-columns.vue



  • demo.vue





          
            
          
          
        
import LfTable from './table'
    import LfTableColumn from './table-column'
    export default {
        name: "demo",
        components: {
            LfTable,
            LfTableColumn
        },
        data() {
            return {
                dataSource: [
                    {id: 1, name: '发发', score: 100, age:18, description: '你最帅,将来一定会成为一个了不起的演员'},
                    {id: 2, name: '琳琳', score: 99, age: 16, description: '为啥不做我媳妇'},
                    {id: 3, name: '西西', score: 99, age: 20, description: '好累啊'},
                    {id: 4, name: '泳儿', score: 99, age: 21},
                    {id: 5, name: '美美', score: 99, age: 22},
                    {id: 6, name: '阿宇', score: 99, age: 26},
                    {id: 7, name: '泳儿', score: 99, age: 21},
                    {id: 8, name: '美美', score: 99, age: 22},
                    {id: 9, name: '阿宇', score: 99, age: 26}
                ],
                selectedItem: [],
            }
        },
  • table.vue

data() {
+ columns: []
},
mounted() {
  // this.$slots遍历拿到每一个table-columns组件
            this.columns = this.$slots.default.map(node => {
                // node.componentOptions.propsData拿到组件中外界传进来的props的值
                let { text, field, width } = node.componentOptions.propsData
                // 如果组件里面使用了插槽那么就可以拿到插槽里对应的render函数也就是
                // 
                let render = node.data.scopedSlots && node.data.scopedSlots.default
                return {
                    text, field, width, render
                }
            })
    // 这里之所以要render里面传一个对象key是value,是因为我们前面写的scope.value,
// scope就是我们的形参也就是下面的{value:'立发'}
      let result = this.columns[0].render({value: '立发'})
       console.log(result)
}

问题: 上面的代码如果直接在原来的td渲染的时候修改会报错

单独将this.columns[0].render({value: '立发'})打印我们发现,他是一个Vnode

问题:那么我们该如何将vnode展示在template里哪
方法:
(1).声明一个vnodes组件如下

components: {
            LfIcon,
            vnodes: {
                function: true,
                render: (h, ctx) => ctx.props.vnodes
            }
        },

(2).对vnodes组件传入一个vnodes,绑定的数据就是你要传入的value


你可能感兴趣的:(造轮子-table组件的实现)