前不久针对公司的后台系统,开发了一套后台系统的组件库。对于table组件的实现思路和细节想分享一下。如果大家有其它的思路和想法,可以评论区留言讨论。
组件传递属性和事件方法
在开始编写组件之前我们要先思考一下,el-table
组件可以传递很多属性和事件方法,我们不可能把它们都一个个罗列到组件中,那有什么办法可以解决这个问题呢?这就引申出vue中两个属性 $attrs
和 $listeners
,它们在封装高级别组件时非常有用。我们先来了解一下这两个属性:
-
$listeners
:包含了父作用域中的(不含.native
修饰器的)v-on
事件监听器,可以通过v-on="$listeners"
传入内部组件 -
$attrs
: 包含了父作用域中不作为prop被识别和获取的attribute绑定(class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件。通常会和inheritAttrs
一起使用。inheritAttrs
属性默认值为true
,$attrs
中的属性会被作为普通的属性回退给子组件的根元素。// 父组件 <son name="张三" :age="18" say="Hello World"></son> // 子组件 export default { props: { name: String, }, created () { console.log(this.$attrs) } }
当把
inheritAttrs
设置为false
时,根元素上就不会绑定age
和say
属性了。
组件的实现
column 需要包含的属性
因为 el-table
被封装起来,所以标题列就需要变成数组columns传递到组件内部进行遍历渲染了。
column 中包含的属性可以分成 原始属性 和 自定义属性:
-
原始属性就是
el-table-column
包含的属性,比如:prop、label、width
等。这些原始属性可以放在名为attrs
的对象中。attrs: { prop: '', label: '', width: '', 'render-header': function(){} }
-
自定义属性包含的情况:
- 标题列可以通过
slot
设置自定义展示内容,所以要设置一个slot:'slotName'
属性 - 标题列可能存在子项,所以要设置一个
children:[]
属性 - 嵌套的子项也包含通过
slot
设置的自定义展示内容,所以要设置一个slotChildren: ['slotName', 'slotName']
(ps:之后会解释如此设置的原因) - 通过
slot='header'
自定义表头内容,需要设置一个slotHeader=true
属性
所以标题列包含的属性有:
columns: [ { attrs: {}, slot: slotName, children: [], slotChildren: [slotName], slotHeader: true } ]
- 标题列可以通过
render渲染函数登场
因为 column
存在子项的情况,使用 template
模板无法渲染子项,而 render
渲染函数可以发挥JavaScript的编程能力,可以很好的代替模板形式。不熟悉 render 渲染函数的,可以到官方文档了解一下使用教程。
话不多说直接上代码:
const RenderTableColumn = {
props: {
column: {
type: Object,
}
},
render(createElement) {
// createElement函数的第二个参数是模板中 attribute 对应的数据对象,包含一个scopedSlots属性
// createScopedSlots方法会生成这个属性的值
const createScopedSlots = (slot, slotHeader) => {
let scopedSlots = {}
// 自定义header存在
// 在element-ui中的el-table-column组件上,可以设置名为header的slot
// 所以相应的数据对象中属性名要为header
if (slotHeader) {
Object.assign(scopedSlots, {
// $scopedSlots.header是对应渲染时的v-slot:header
header: scope => this.$scopedSlots.header(scope)
})
}
// 自定义内容slot存在
// 在element-ui中的el-table-column组件上,可以设置非具名slot
// 解析时非具名的slot对应的名称是default,所以内部属性名就是default
if (slot) {
Object.assign(scopedSlots, {
default: scope => {
// 因为在调用组件时可能传入多个具名插槽,所以要使用动态属性名
if (this.$scopedSlots[slot]) {
return this.$scopedSlots[slot](scope)
}
}
})
}
return { scopedSlots }
}
// 生成最终column组件的方法
const renderColumn = column => {
// 获取属性
const { attrs, slot, slotHeader, children } = column
// 保存在createElement函数第二个对象参数中,经测试用props和attrs都可以
let props = { props: { ...attrs } } // attrs: {...attrs}
// 获取自定义slot
Object.assign(props, createScopedSlots(slot, slotHeader))
// 递归遍历嵌套表头
const hasChildren = Array.isArray(children) && children.length
// nodes是子级虚拟节点数组
const nodes = hasChildren ? children.map(col => {
return renderColumn(col)
}) : []
return createElement(TableColumn, props, nodes)
}
return renderColumn(this.column)
}
}
CustomTable组件的实现
先上 CustomTable 组件代码(如果大家要使用的话,可以在此基础上添加自己想要的功能):
<template>
<div class="CustomTable">
<el-table v-bind="$attrs" v-on="$listeners" ref="CustomTable">
<template v-for="(column, index) in columns">
<render-table-column
v-if="column.slotHeader || column.slot || column.slotChildren"
:key="column.attrs.prop"
:column="column">
<!-- 自定义header -->
<template
v-if="column.slotHeader"
v-slot:header="scope">
<slot :name="column.attrs.prop + 'Header'" :scope="{...scope, index}" />
</template>
<!-- 设置slot属性 -->
<template
v-if="column.slot"
v-slot:[column.slot]="scope">
<slot :name="column.slot" :scope="{...scope, index}">
<!-- 没有传slot时的默认值 -->
{{scope.row[column.attrs.prop]}}
</slot>
</template>
<!-- 子表头有自定义slot -->
<template
v-if="column.slotChildren && column.slotChildren.length"
v-for="name in column.slotChildren"
v-slot:[name]="scope">
<slot :name="name" :scope="{...scope, index}"></slot>
</template>
</render-table-column>
<render-table-column
v-else
:column="column"
:key="column.attrs.prop">
</render-table-column>
</template>
</el-table>
</div>
</template>
<script>
import { Table } from 'element-ui'
export default {
name: 'CustomTable',
components: {
ElTable: Table,
RenderTableColumn
},
inheritAttrs: false,
props: {
// 传入属性同el-table-column组件属性
columns: {
type: Array,
required: true,
default: () => {
return []
}
},
tableRef: {
type: Object
},
},
mounted () {
this.$emit('update:tableRef', this.$refs.CustomTable)
}
}
</script>
Ps: 对于 el-table 的ref可以在使用组件时,通过 :tableRef.sync="tableRef"
获取,然后通过 this.tableRef
调用 Table Methods里的方法。
组件的使用方法如下:
<template>
<custom-table
:data="tableData"
:columns="columns"
:tableRef.sync="tableRef">
<template v-slot:name="{scope}">
<span style="color:#ff8f0a">{{scope.row.detail}}</span>
</template>
<template v-slot:city="{scope}">
<el-tag>{{scope.row.city}}</el-tag>
</template>
</custom-table>
</template>
<script>
export default {
component: {
CustomTable
},
data () {
return {
tableRef: null,
tableData: [
{
date: '2016-05-03',
name: '张三',
province: 'XXX省',
city: 'XXX市',
detail: 'XXX区',
address: 'XXX省XXX市XXX区'
},
],
columns: [
{
attrs: {prop: "date", label: "日期", width: "100"},
slotHeader: true
},
{
attrs: {prop: "name", label: "姓名", width: "80"},
slot: 'name'
},
{
attrs: {prop: "address", label: "地址", width: '260'},
// 对应着子项中的slot属性
slotChildren: ['city'],
children: [
{attrs: {prop: "province", label: "省份", width: "70"}},
{
attrs: {prop: "city", label: "城市", width: "80"},
slot: 'city'
}
]
}
],
}
}
}
</script>
生成的结果为:
为什么子项有自定义内容时设置slotChildren
先看一下上面使用组件时的代码,所有的 **slot **插槽都是在 CustomTable 的作用域下定义的,了解了它以后我们往下看不同的情况。
-
不设置 slotChildren 属性:
<template v-if="column.slotChildren && column.slotChildren.length" v-for="name in column.slotChildren" v-slot:[name]="scope"> <slot :name="name" :scope="{...scope, index}"></slot> </template>
上面这段代码不会执行。
<template v-slot:city="{scope}"> <el-tag>{{scope.row.city}}</el-tag> </template>
v-slot=city
插槽是在address这个列的作用域中,在渲染address
列时不存在slotChildren
属性,那city
的插槽会被忽略掉不会被渲染。在列循环渲染到city
时,因为city
的插槽没有被添加到$scopedSlots
属性中取不到值,所以city
列在页面中是没有数据的。 -
设置
slotChildren
属性:如果设置了
slotChildren
属性,那么上面v-if
条件成立,city
插槽就会被渲染并添加到$scopedSlots
属性中。在列循环渲染到city
时,就可以在$scopedSlots
属性中找到对应的slot。
Ps:在子项中使用 slotHeader: true
属性自定义头部是无效的,目前还没有想到解决办法。如果大家有解决的办法,欢迎指教。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!