具名插槽是vue中非常重要的一个概念,常用于我们在封装组件的时候,今天我们由浅入深来研究一下它。
1. 通过具名插槽在父组件中自定义显示列的内容
比如我们在封装表格组件时,经常会运用具名插槽在父组件中通过列名去自定义单元格内容,下面我以vxe-table表格为列。
- 子组件
> <vxe-table
border
resizable
auto-resize
show-overflow
column-key
:row-id="rowId"
:columnConfig="{ useKey: true }"
:edit-rules="validRules"
:size="size"
:expandConfig="{expandRowKeys:this.expandRowKeys,reserve:true}"
:edit-config="{ trigger: 'click', mode: 'row', showIcon: false }"
:checkbox-config="{ checkRowKeys: checkedData, checkMethod: checkBoxMethod }"
:data="tableData"
max-height="500px"
style="width:100%"
:row-style="setRowStyle"
:row-config="{ isHover: isHoverShow,keyField:'id' }"
@cell-dblclick="cellClick"
@checkbox-change="handleSelectionChange"
@checkbox-all="handleSelectionChange"
:show-footer="isShowFooter"
:footer-method="footerMethod"
@toggle-row-expand="handleExpand"
ref="vxeTable"
v-bind="vxeTableAttr"
>
<template v-for="item in tableColumntable">
<vxe-table-colgroup v-bind="item" :key="item.field" v-if="item.children">
<template v-for="subCol in item.children">
<vxe-table-column v-bind="subCol" :key="subCol.field" >
<template #defalut="{ row }">
{{ row[subCol.field] }}
</template>
</vxe-table-column>
</template>
</vxe-table-colgroup>
<vxe-table-column
:key="item.field"
v-bind="item"
:type="item.type"
v-else-if="item.type === 'expand'"
>
<template #default="{ row }">
<span>{{ row[item.field] }}</span>
</template>
</vxe-table-column>
<vxe-table-column :key="item.field" v-bind="item" v-else-if="item.type === 'seq'">
<template #default="{ row, rowIndex }">
<span>{{ row.footFlag ? '小计' : rowIndex + 1 }}</span>
</template>
</vxe-table-column>
<vxe-table-column :key="item.field" v-bind="item" v-else>
<template slot-scope="scope">
//重点:插槽部分
<template v-if="$scopedSlots[item.field]">
<slot :name="item.field" v-bind="scope"></slot>
</template>
<span v-else-if="item.formatter">
{{
item.formatter({
row: scope.row,
column: scope.column,
cellValue: scope.row[item.field],
})
}}
</span>
<span v-else>
{{ scope.row[item.field] }}
</span>
</template>
</vxe-table-column>
</template>
</vxe-table>
</template>
<script>
export default {
name: 'VxeChildTableView',
props: {
tableColumn: {
type: Array,
required: true,
},
tableData: {
type: Array,
required: true,
},
size: {
type: String,
default: 'mini',
},
showCheckBox: {
type: Boolean,
default: true,
},
// 是否支持行点击事件
isCellCanClick: {
type: Boolean,
default: false,
},
// 是否支持行鼠标经过时高亮
isHoverShow: {
type: Boolean,
default: false,
},
vxeTableAttr: {
type: Object,
default: () => ({}),
},
rowId: {
type: String,
default: 'id',
},
expandRowKeys:{
type: Array,
default: () => [],
},
checkedData: {
type: Array,
default: () => [],
},
checkBoxMethod: { type: Function },
validRules: {
type: Object,
default: () => {},
},
moduleName: {
type: String,
default: 'purchase_vxeTableView',
},
editMode: {
type: Boolean,
default: true,
},
isShowFooter:{
type: Boolean,
default: false,
},
footerMethod:Function,
},
watch:{
tableColumn:{
deep:true,
immediate:true,
handler(val){
let seq = [
{
type: 'seq',
field: 'seq',
title: '序号',
width: 55,
fixed: 'left',
align: 'center',
},
]
let checkbox = [
{
type: 'checkbox',
field: 'checkbox',
width: 35,
fixed: 'left',
align: 'left',
},
]
this.tableColumntable = this.showCheckBox ? [...seq,...checkbox,...val] : [...seq,...val]
}
},
},
data() {
return {
selectRowIndex: 0,
tableColumntable:[]
};
},
methods: {
handleExpand({row,expand}){
this.$emit('handleExpand',{row,expand})
},
handleSelectionChange(data) {
const checked = this.$refs.vxeTable.getCheckboxRecords();
this.$emit('handleSelectionChange', checked, data);
},
setRowStyle({ rowIndex }) {
if (this.isCellCanClick) {
if (rowIndex === this.selectRowIndex) {
return 'background-color: #F6F6FE;color: #4869F4;';
}
}
},
cellClick({ row, rowIndex, column }) {
if (column.type === 'checkbox' || !this.isCellCanClick) return;
this.selectRowIndex = rowIndex;
this.$emit('currentChart', row);
},
setChekboxRow(data) {
this.$refs.vxeTable.setCheckboxRow(data, true);
},
refreshTable(newData) {
this.$refs.vxeTable.loadData(newData);
},
validateTable() {
return new Promise((resolve, reject) => {
// vxe表格校验
this.$refs.vxeTable.validate(true, (err) => {
if (err) {
this.$nextTick(() => {
reject();
});
} else {
resolve(true);
}
});
});
},
},
};
</script>
- 父组件
在父组件中我们就可以通过 <template #warehouseIds="{row,rowIndex}" >这种方式去自定义列名是warehouseIds那列的显示内容,这就是具名插槽在组件中的使用方式
:tableData="saleOrderData"
:tableColumn="saleOrderTableColumn"
:editMode="false"
:showCheckBox="false"
:isShowFooter="true"
:footerMethod="footerMethod"
:footerChildMethod="footerChildMethod"
v-loading="tableLoading"
moduleName="sale_order_inventory_goods_table"
>
<template #warehouseIds="{row,rowIndex}" >
<el-select
v-model="row.warehouseIds"
filterable
multiple
collapse-tags
placeholder="请选择"
style="width: 100%;"
@change="freshChildData(rowIndex)"
>
<el-option
v-for="(item, index) in wareHouseList"
:key="item.id + index"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</template>
</VxeChildTableView>
现在需求突然变了,要求表格要显示成那种能展开的表格,并且主子表都能在父组件中自定义列的显示内容,比如下面这样的,这就是我们今天要讲的重点
- 通过具名插槽在父组件中自定义显示展开表格列的内容
于是我把子组件改成递归组件,注意看下面代码块中这段内容`` <template v-for="(_, slotName) in $scopedSlots" :slot="slotName" slot-scope="scope"> <slot :name="slotName" v-bind="scope" /> </template>``这段代码正是具名插槽能随着递归组件传到最内层组件的关键
- 改动后子组件
border
resizable
auto-resize
show-overflow
column-key
:row-id="rowId"
:columnConfig="{ useKey: true }"
:edit-rules="validRules"
:size="size"
:expandConfig="{expandRowKeys:this.expandRowKeys,reserve:true}"
:edit-config="{ trigger: 'click', mode: 'row', showIcon: false }"
:checkbox-config="{ checkRowKeys: checkedData, checkMethod: checkBoxMethod }"
:data="tableData"
max-height="500px"
style="width:100%"
:row-style="setRowStyle"
:row-config="{ isHover: isHoverShow,keyField:'id' }"
@cell-dblclick="cellClick"
@checkbox-change="handleSelectionChange"
@checkbox-all="handleSelectionChange"
:show-footer="isShowFooter"
:footer-method="footerMethod"
@toggle-row-expand="handleExpand"
:ref="dynamicRef"
v-bind="vxeTableAttr"
>
<template v-for="item in tableColumntable">
<vxe-table-colgroup v-bind="item" :key="item.field" v-if="item.children">
<template v-for="subCol in item.children">
<vxe-table-column v-bind="subCol" :key="subCol.field" >
<template #defalut="{ row }">
{{ row[subCol.field] }}
</template>
</vxe-table-column>
</template>
</vxe-table-colgroup>
<vxe-table-column
:key="item.field"
v-bind="item"
:type="item.type"
v-else-if="item.type === 'expand'"
>
<template #default="{ row }">
<span>{{ row[item.field] }}</span>
</template>
<template #content="{ row: childRow, rowIndex: childRowIndex }">
<VxeChildTableView
:tableData="childRow.childData"
:tableColumn="expandColumn"
:childRowIndex="childRowIndex"
:isChild="true"
:isShowFooter="true"
:showTableSetting="false"
:footerMethod="footerChildMethod"
:checkedData="checkedChildData"
@handleSelectionChange="(data)=>handleChildSelectionChange(data,childRowIndex)"
>
//重点部分:具名插槽透传
<template v-for="(_, slotName) in $scopedSlots" :slot="slotName" slot-scope="scope">
<slot :name="slotName" v-bind="scope" />
</template>
</VxeChildTableView>
</template>
</vxe-table-column>
<vxe-table-column :key="item.field" v-bind="item" v-else-if="item.type === 'seq'">
<template #default="{ row, rowIndex }">
<span>{{ row.footFlag ? '小计' : rowIndex + 1 }}</span>
</template>
</vxe-table-column>
<vxe-table-column :key="item.field" v-bind="item" v-else>
<template slot-scope="scope">
<template v-if="$scopedSlots[item.field]">
<slot :name="item.field" v-bind="scope"></slot>
</template>
<span v-else-if="item.formatter">
{{
item.formatter({
row: scope.row,
column: scope.column,
cellValue: scope.row[item.field],
})
}}
</span>
<span v-else>
{{ scope.row[item.field] }}
</span>
</template>
</vxe-table-column>
</template>
</vxe-table>
export default {
name: 'VxeChildTableView',
props: {
tableColumn: {
type: Array,
required: true,
},
tableData: {
type: Array,
required: true,
},
size: {
type: String,
default: 'mini',
},
showCheckBox: {
type: Boolean,
default: true,
},
// 是否支持行点击事件
isCellCanClick: {
type: Boolean,
default: false,
},
// 是否支持行鼠标经过时高亮
isHoverShow: {
type: Boolean,
default: false,
},
isChild: {
type: Boolean,
default: false,
},
childRowIndex: { type: Number },
vxeTableAttr: {
type: Object,
default: () => ({}),
},
rowId: {
type: String,
default: 'id',
},
expandRowKeys:{
type: Array,
default: () => [],
},
checkedData: {
type: Array,
default: () => [],
},
checkedChildData: {
type: Array,
default: () => [],
},
expandColumn: {
type: Array,
default: () => [],
},
checkBoxMethod: { type: Function },
validRules: {
type: Object,
default: () => {},
},
moduleName: {
type: String,
default: 'purchase_vxeTableView',
},
showTableSetting: {
type: Boolean,
default: true,
},
editMode: {
type: Boolean,
default: true,
},
isShowFooter:{
type: Boolean,
default: false,
},
footerMethod:Function,
footerChildMethod:Function,
},
computed: {
dynamicRef() {
return this.isChild ? `vxeTableChild${this.childRowIndex}` : 'vxeTable';
},
columnsConfig(){
if(!this.showTableSetting || this.isChild) return;
return{
productName: 'openerp',
moduleName: this.moduleName,
userId: getCookie('_name_'),
list: {
name: 'vxeTable',
prop: 'tableColumntable',
hasExtendedField: true,
methods: { dropCallBack: this.handleColumnDrop },
},
}
}
},
watch:{
tableColumn:{
deep:true,
immediate:true,
handler(val){
let seq = [
{
type: 'seq',
field: 'seq',
title: '序号',
width: 55,
fixed: 'left',
align: 'center',
},
]
let checkbox = [
{
type: 'checkbox',
field: 'checkbox',
width: 35,
fixed: 'left',
align: 'left',
},
]
this.tableColumntable = this.showCheckBox ? [...seq,...checkbox,...val] : [...seq,...val]
}
},
},
data() {
return {
selectRowIndex: 0,
tableColumntable:[],
};
},
methods: {
handleExpand({row,expand}){
this.$emit('handleExpand',{row,expand})
},
handleSelectionChange(data) {
if (this.isChild) {
this.$emit(
'handleSelectionChange',
this.$refs[`vxeTableChild${this.childRowIndex}`].getCheckboxRecords(),
);
} else {
const checked = this.$refs.vxeTable.getCheckboxRecords();
this.$emit('handleSelectionChange', checked, data);
}
},
handleChildSelectionChange(data,index){
this.$emit('handleChildSelectionChange',data,index)
},
setRowStyle({ rowIndex }) {
if (this.isCellCanClick) {
if (rowIndex === this.selectRowIndex) {
return 'background-color: #F6F6FE;color: #4869F4;';
}
}
},
cellClick({ row, rowIndex, column }) {
if (column.type === 'checkbox' || !this.isCellCanClick) return;
this.selectRowIndex = rowIndex;
this.$emit('currentChart', row);
},
setChekboxRow(data) {
this.$refs.vxeTable.setCheckboxRow(data, true);
},
setChildChekboxRow(data){
console.log(38,data)
this.$refs[this.dynamicRef].setCheckboxRow(data, true);
},
refreshTable(newData) {
this.$refs.vxeTable.loadData(newData);
},
scrollToPosition(position){
this.$refs[this.dynamicRef].scrollTo(position)
},
validateTable() {
return new Promise((resolve, reject) => {
// vxe表格校验
this.$refs.vxeTable.validate(true, (err) => {
if (err) {
this.$nextTick(() => {
reject();
});
} else {
resolve(true);
}
});
});
},
},
};
- 改动后父组件调用
js
<VxeChildTableView
:tableData="saleOrderData"
:tableColumn="saleOrderTableColumn"
:expandColumn="saleOrderExpandColumn"
:editMode="false"
:showCheckBox="false"
:expandRowKeys="expandRowKeys"
:isShowFooter="true"
:footerMethod="footerMethod"
:footerChildMethod="footerChildMethod"
:checkedChildData="checkRowKeys"
@handleExpand="handleExpand"
v-loading="tableLoading"
moduleName="sale_order_inventory_goods_table"
ref="vxeTable"
@handleChildSelectionChange="handleSelectInventory"
>
//主表插槽传入的自定义内容
<template #warehouseIds="{row,rowIndex}" >
<el-select
v-model="row.warehouseIds"
filterable
multiple
collapse-tags
placeholder="请选择"
style="width: 100%;"
@change="freshChildData(rowIndex)"
>
<el-option
v-for="(item, index) in wareHouseList"
:key="item.id + index"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</template>
// 展开表格插槽传入的自定义内容
<template #spotOrderChildWeight="{row}" >
<el-input type="number" placeholder="请输入" @change="(val)=>handleSpotOrderChildWeight(row,val)" v-model="row.spotOrderChildWeight" clearable />
</template>
<template #spotOrderChildNum="{row}" >
<el-input type="number" placeholder="请输入" @change="(val)=>handleSpotOrderChildNum(row,val)" v-model="row.spotOrderChildNum" clearable />
</template>
</VxeChildTableView>
说明:在vue2中可以通过$scopedSlots获取所有父组件中传来的插槽,在vue3中则是通过$slots获取