概述
本文主要进行描述一种在vue中封装表格的方法。目标是达成类似于element-plus中的使用方式。element-plus中表格用法如下:
<template>
<el-table :data="tableData">
<el-table-column prop="id" label="Id" />
<el-table-column prop="name" label="Name" />
<el-table-column prop="age" label="年龄">
<template #default="scope">
<span>{{scope.row.age}}岁</span>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button @click="handleClick(scope.row)">点击</el-button>
</template>
</el-table-column>
</el-table>
</template>
方案
从element-plus的表格用法可以看出,在组件el-table
与组件el-table-column
中存在插槽,即使用了slot
标签。故在我们的组件中table
模板结构应当如下:
方案一(简单封装)
table(NavigationTable.vue):
<template>
<table>
<thead>
<tr>
<slot name="header"/>
</tr>
</thead>
<tbody>
<tr v-for="(row,index) in data" :key="index">
<slot :row="row" :index="index" />
<tr>
</tbody>
</table>
</template>
<script>
export default {
name:'na-table',
props: {
data: {
type: Array,
default:() => []
}
}
}
</script>
这种写法是不必需要table-column
组件的,因为标签slot
中的属性值必须要显示的调用才行因此会变成以下情况:
<template>
<na-table :data="tableData">
<template #header>
<th>Id</th>
<th>Name</th>
<th>年龄</th>
<th>操作</th>
</template>
<template #default="scope">
<td>{{scope.row.id}}</td>
<td>{{scope.row.name}}</td>
<td>{{scope.row.age}}岁</td>
<td><button @click="handleClick(scope.row)">点击</button></td>
</template>
</na-table>
</template>
<script>
import NaTable from '@/components/NavigationTable.vue';
export default {
name:'Test',
data(){
return {
tableData:[{
id: 1,
name: 'test1',
age: 20
}]
}
},
methods: {
handleClick(row){
row.age = row.age+1;
}
}
}
</script>
可以说这种封装几乎就是毫无意义可言,与封装前相比只是少些了一些HTML标签,同时将表头与对应的列割裂开,非常的不方便。那是否可以改写成下面这种形式呢?
错误方案
table
...
<thead>
<tr>
<slot column-type="header" />
</tr>
</theda>
···
···
<tbody>
<tr v-for="(row,index) in data" :key="index">
<slot column-type="body" :row="row" :index="index" />
<tr>
</tbody>
···
table-column
<template>
<th v-if="columnType=='header'">{{label}}</th>
<td v-if="columnType=='body'">{{row[prop]}}</th>
</template>
<script>
export default {
props:{
columnType:{
type:String,
default: ''
},
label:{
type:String,
default:''
},
prop:{
type:String,
default:''
},
row:{
type:Object,
default:()=>{}
}
}
}
</script>
即我们通过在slot
标签中增加一个columnType属性将其传递给column组件以实现在将表头和列组合在一起。但这种方式很显然是不可行的,因为slot
属性是无法直接传递给被插入的组件,只能通过以下这种方式获取,即#slotName="data"
的形式传递,至于等于后面是什么并不重要,可以较scope,也可以叫data或者slotProps都可以。
<template #default="scope"></template>
那么是否可以直接将子组件的template
标签或者在子组件中的子标签写成上述的那种形式以直接获取数据呢?答案是同样不行!
element-ui封装方法
在element-ui中组件el-table-column
并不是一个带有样式的组件,而是一个逻辑组件。即通过这个组件记录列的信息,再将这个列信息返回到el-table
组件,然后el-table
组件再通过js操纵dom的方式去展示table。具体参考如下:
从源码看Element UI Table组件实现思路
从源码看Element UI Table组件实现思路(github)
方案二(warp封装)
前面说了,直接通过在标签slot
中增加属性无法直接将值传递到子组件中,那么能否通过一些方法使得被插入插槽的子组件获取到父组件想要传递过来的参数呢?这样我们的错误方案即可变成正确方案了!这个方法确实是有的,即通过this.$parent
即可。具体方法如下:
table(NavigationTable.vue)
<template>
<div class="table">
<table class="table-table">
<thead>
<tr>
<!--为slot增加一个包装,将属性传递给组件na-table-column-warp -->
<na-table-column-warp column-type="header">
<slot />
</na-table-column-warp>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="index">
<!--为slot增加一个包装,将属性传递给组件na-table-column-warp -->
<na-table-column-warp column-type="body" :row="row" :index="index">
<slot />
</na-table-column-warp>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import NaTableColumnWarp from './NavigationTableColumnWarp.vue';
export default {
name: "table",
components: {
NaTableColumnWarp,
},
props: {
data: {
type: Array,
default: () => []
},
},
}
</script>
table-column-warp(NavigationTableColumnWarp.vue)
<template>
<!--用于插入table-column组件-->
<slot/>
</template>
<script>
export default {
name: 'NavTableColumnWarp',
props: {
//列的类型
columnType:{
type: String,
default: '',
},
//当前行的数据
row:{
type: Object,
default: () => {},
},
//当前行的索引
index:{
type: Number,
default: 0,
},
},
}
</script>
table-column(NavigationTableColumn.vue)
<template>
<!--如果是thead则显示以下内容-->
<template v-if="columnType == 'header'">
<th :style="Style">
<!--表头插槽,如有则默认优先使用插槽-->
<div v-if="$slots.header">
<slot name="header"></slot>
</div>
<div v-else>
{{label}}
</div>
</th>
</template>
<!--如果是tbody则显示以下内容-->
<template v-if="columnType == 'body'">
<td :style="Style">
<!--cell插槽,如有则默认优先使用插槽-->
<div v-if="$slots.default">
<slot :row="row" :index="index"></slot>
</div>
<div v-else>
{{data}}
</div>
</td>
</template>
</template>
<script>
import getDeepObjectValue from '../utils/getDeepObjectValue.js'
export default {
name: 'NavigationTableColumn',
props: {
//表头标签
label:{
type: String,
default: '',
},
//展示的属性名
prop:{
type: String,
default: '',
},
//列宽度
width:{
type: String,
default: '100px',
},
//对齐方式
align:{
type: String,
default: 'center',
},
},
data(){
return{
columnType: '',
index: 0,
row: {},
}
},
mounted() {
//从父组件table-column-warp处获取类型
this.columnType = this.$parent.columnType;
//从父组件table-column-warp处获取行数据
this.row = this.$parent.row;
//从父组件table-column-warp处获取索引
this.index = this.$parent.index;
},
computed: {
data(){
//获取深层对象属性,处理类似'a.b.c'的属性名,具体函数见下
return getDeepObjectValue(this.row, this.prop);
},
Style(){
return "width: " + this.width + "; text-align: " + this.align + ";";
}
},
}
</script>
getDeepObjectValue.js
export default function getDeepObjectValue(obj, path) {
if (Array.isArray(path)) {
path = path.join(".");
}
if (!obj) return undefined;
try {
if (!path) return obj;
return path.split(".").reduce((o, i) => o[i], obj);
} catch (error) {
return undefined;
}
}
使用方法
<template>
<div>
<nav-table :data="tableData">
<nav-table-column label="Id" prop="id" />
<nav-table-column label="Name" prop="name" />
<nav-table-column label="年龄" prop="age" >
<template #default="scope">
<span>{{scope.row.age}}岁</span>
</template>
</nav-table-column>
<nav-table-column label="操作" >
<template #default="scope">
<button @click="handleClick(scope.row)">点击</button>
</template>
</nav-table-column>
</nav-table>
</div>
</template>
<script>
import NavTableColumn from '@/components/NavigationTableColumn.vue';
import NavTable from '@/components/NavigationTable.vue';
export default {
components: {
NavTableColumn,
NavTable,
},
data(){
return{
tableData:[{
id: 1,
name: 'test1',
age: 20,
}]
}
},
methods: {
handleClick(row){
row.age = row.age + 1;
}
}
}
</script>