接上篇文章👉手把手教你玩转render函数「组件封装-dynamic-select」(上)
自定义slots
Select Slots
提供了prefix/empty
这两个内置插槽,所以我们基于el-select
封闭的组件需要支持用户自定义插槽内容
配置项
先根据目前提供配置项实现组件基础功能
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
slots | 插槽 | Object | {} |
scopedSlots | 作用域插槽 | Object | {} |
render函数
DynamicSelect/select.js
import SlotContent from './slotContent'
export default {
// ...忽略上篇文章定义过的配置项
// 注册一个函数式组件用来渲染提供给组件外层插槽内容
components: { SlotContent },
render(h) {
// 配置插槽 => 用来渲染组件内置的作用域插槽
const slots = Object.keys(self.slots).map(slotName => {
return [h('slot-content', {
props: {
render: self.slots[slotName],
data: self
},
slot: slotName,
key: slotName
})]
})
}
}
下面需要修改渲染el-select
那块代码中createElement
的第三个参数
createElement方法的第三个参数是当前节点的内容, 就类似于div -> 可以放多个span
, 所以第三个参数包括:
- 当前渲染元素(组件)的子节点
- 当前渲染元素(组件)提供的内置作用域插槽(可以通过$scopeSlots识别到)
- 当前渲染元素(组件)的插槽内容slot
return h('el-select', {
class: self.className,
staticClass: 'jl-full-line',
....
}, [Object.keys(this.$slots).map(s => this.$slots[s]), ...optionsVnode, ...slots])
通过上面我们就能将这三种类型的内容都渲染到el-select组件里面
DynamicSelect/select.js
export default {
name: 'SlotContent',
// 声明这是函数式组件
functional: true,
props: {
render: {
type: Function,
require: true
},
data: Object
},
render: (h, ctx) => {
return ctx.props.render(h, ctx.props.data)
}
}
灵活使用render来高度自定义组件
<template>
<DynamicSelect
ref="DynamicSelect"
:value.sync="projectId"
v-bind="projectSelectOption"
:parse-data="parseData"
:formatter="formatterValue"
:multiple="true"
collapse-tags
// 添加自定义slots配置
:slots="customSlots"
>
</DynamicSelect>
</template>
<script>
export default {
data() {
return {
customSlots: {
prefix: this.prefixRender
},
}
},
methods: {
prefixRender(h, vue) {
// h => createElement
// vue => DynamicSelect实例
// 我们可以通h来渲染任意组件到这个自定义插槽里面
return h('div', {
staticClass: 'select-prefix',
},
[h('svg-icon', {
props: {
'icon-class': 'bug'
},
})])
},
}
}
</script>
自定义options
效果图💗
继续完善render函数
+ const { value, label, labelRender } = self.optionsProps
+ // 选项格式化显示
+ let labelRenderNode = null
+ if (labelRender && typeof labelRender === 'function') {
+ labelRenderNode = function(labelValue, op) {
+ return labelRender(h, labelValue, op)
+ }
+ }
// 渲染options
const optionsVnode = self.optionsData.map((op, index) => {
return [h('el-option', {
attrs: {
value: op[value],
label: op[label],
disabled: op.disabled
},
key: op.id || op.value // 绑定唯一key
+ } labelRenderNode ? labelRenderNode(op[label], op) : null)]
})
formatter自定义options内容
<template>
<DynamicSelect
...
// 添加自定义slots配置
:slots="customSlots"
:props="props"
>
</DynamicSelect>
</template>
<script>
// 定义渲染options的props
const props = {
label: 'proName',
value: 'id',
children: 'children',
labelRender(h, label, item) {
// h => createElement
// label => options的label
// item => options的每一项, 也就是每一个项目对象
return [h('span', {
staticClass: 'left',
domProps: {
innerText: label
}
}),
h('span', {
staticClass: 'right',
domProps: {
// 右侧我们展示了项目id的前两位
innerText: item?.id.substr(0, 2) || ''
}
})]
}
}
export default {
data() {
return {
customSlots: {
prefix: this.prefixRender
},
// 通过Object.freeze冻结对象, 避免vue给对象每个成员加上getter/setter (也是个性能优化技巧)
props: Object.freeze(props),
}
},
}
</script>
通过$slots给组件里面加个全选
<template>
<DynamicSelect
...
>
<el-tag
slot="selectAll"
type="primary"
class="select-tag-class"
@click="handleSelectAll"
>
{{ selectAllLabel[selectAll] }}
</el-tag>
</DynamicSelect>
</template>
<script>
export default {
data() {
return {
selectAllLabel: {
false: '全选',
true: '取消',
},
// 默认非全选
selectAll: false,
}
},
methods: {
// 全选Or取消
handleSelectAll() {
this.selectAll = !this.selectAll
const list = []
if (this.selectAll) { // 全选
} else { // 取消
}
// 通过$refs将内容
this.$refs.DynamicSelect.newValue = list
}
}
}
</script>
这里就有个问题了, 我们是动态获取的options ,我$refs虽然可以引用DynamicSelect的optionsData属性, 但是我接口拉取需要时间, 假设我走详情页面进来我之前选中的全部,我要让选项全部选中怎么搞 , tip -> (我们项目选择全部的时候传给后端的是一个
[]
)
为什么不把所有项目的id全传进去???
/*
这里设计到一个动态响应的问题, 假设我当时新增单据的时候总共有4个项目ids: [A, B, C, D]
后面我新增了两个项目 [E, F]
但是我之前存给后端的还是4个项目的id,所以当时有个项目中就是这么搞的
*/
回到之前的问题,动态拉取的options我怎么及时响应呢? 👉发布订阅
发布订阅
很多种实现方式,这里用类来写
./DynamicSelect/Publish.js
// 发布订阅模式(解决options异步拉取数据的问题)
export default class Publish {
constructor() {
this.pond = []
}
on(callBack) {
this.pond.push(callBack)
}
emit(name) {
// 传递回调名称,有匹配的就将回调执行
const fn = this.pond.find(c => c.name === name)
fn && fn()
}
}
给之前的方法加个自定义事件
// 动态拉取选项数据
async getOptionsData() {
this.$emit('getOptionsSuccess', this.optionsData)
}
监听这个自定事件
getOptionsSuccess(data) {
// 外层存储一份options的引用地址
this.selectOptions = data
}
完善handleSelectAll
export default {
import Publish from './DynamicSelect/Publish.js'
const subscribe = new Publish()
methods: {
handleSelectAll() {
// 可以方法内部往事件池里push回调,也可以在调用方法push(看执行时机)
this.$nextTick(() => {
this.selectAll = !this.selectAll
const list = []
if (this.selectAll) { // 全选
list = this.selectOptions.map(i => i.id)
} else { // 取消
list = []
}
// 主动将组件全选 ->
// $refs引用到组件的newValue直接赋值
// 直接变动value 推荐这个
this.porjectId = list
})
}
}
}
至此组件封装就完事了,还有远程搜索那块没有写上来,发现写这种文章不太好写,代码太多又怕读者看起来发懵,不上代码干讲也感觉很难讲明白。比较难权衡,这也是我之前文章【以模块化的思想开发中后台项目】迟迟没有后续更新的原因,不过我也一直在学习这方面,这个系列后续还是会更的。
tip: 本组件用render函数封闭并不是因为有多高级,render函数允许我们通过写js的方式来构建组件,相较于模板语法确实灵活不少,但是也需要付出相应的代价
很多Vue提供的语法糖是用不了的,需要自己去实现,所以需要根据组件功能各自取舍
写在最后
如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下
我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。
往期文章
【前端体系】从一道面试题谈谈对EventLoop的理解(更新了四道进阶题的解析)