Vue手搓轮子系列——表格封装(一)

概述

本文主要进行描述一种在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>

效果图

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值