theme: juejin highlight: atelier-estuary-light
先来看下效果👇
上面展示了select
组件的两种很常见的使用常见
- 1.表格的过滤(根据项目过滤数据)
- 2.表单的使用(基于项目来创建单据)
组件用例
先给份用例了解下组件大致提供那些功能
- 支持格式化选项数据
- 自定义事件回调
listeners
- 插入组件提供的作用域插槽
<template>
<DynamicSelect type="select" v-model="value" v-bind="config">
</DynamicSelect>
<div>选中的值:{{value}}</div>
</template>
<script>
import '@/components/FormSelect'
export default {
data() {
return {
value: '',
config: {
clearable: true,
filterable: true,
options: [
{
name: '选项一',
id: '1',
number: 'FFF1'
},
{
name: '选项二',
id: '2',
number: 'SSS2'
},
{
name: '选项三',
id: '3',
number: 'TTT3',
disabled: true
},
{
name: '选项四',
id: 4,
number: 'LLL4'
}
],
props: {
label: 'name',
value: 'id',
formatter: (val, op) => {
return `${val} ${op.number}`
}
},
multiple: true,
listeners: {
change: (val) => {console.log('change ===> ', val)},
dataChange: (val) => {console.log('dataChange ===> ', val)},
},
slots: {
prefix: (h) => (<i class='el-icon-edit el-input__icon' />)
}
}
}
},
}
</script>
列表过滤使用
对于列表根据项目过滤很多地方都用到,我们可以选择定义一个mixin
文件来简化每个页面的配置操作
DynamicSelectOptions.js
import { requestUrl } from '@/api/constant'
const DEFAULT_PAGE = 1
const DataTableMixin = {
data() {
return {
_url: '',
_params: {},
_query: {},
page: DEFAULT_PAGE,
offset: 20,
loading: false, // 是否在加载数据
list: [], // 数据列表
total: 0,
_dataField: null,
projectSelectOption: {
url: requestUrl.projectSettingsList,
props: {label: 'proName',value: 'id'},
params: {status: 1, pageSize: 1000, pageIndex: 1},
searchKey: "projectName",
clearable: true,
filterable: true
}
}
},
methods: {
/**
* 拉取数据
* @returns {Promise<any>}
*/
async dataTableInit() {
// 根据url拉取数据
},
}
}
export default DataTableMixin
A页面
<template class="A app-container">
<FormSelect
ref="FormSelect"
value.sync="projectId"
v-bind="projectSelectOption"
:clearable="false"
// 如果不允许清除选项,可以通过下面的覆盖上面v-bind绑定的配置项
// 类似 Object.assing({clearable: true}, {clearable: false})
>
</FormSelect>
</template>
<script type="text/ecmascript-6">
import '@/components/FormSelect'
import DynamicSelectOptions form './DynamicSelectOptions'
export default {
data() {
reutrn {
projectId: '',
}
}
}
</script>
配置项
先根据目前提供配置项实现组件基础功能
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
value | 数据 | [String , Boolean , Number , Array ] |
- |
formatter | 格式化数据 | Function |
- |
disabled | 是否禁用 | Boolean |
false |
readonly | 是否只读 | Boolean |
false |
options | 备选项 | Array |
- |
props | 备选项映射 | Object |
{label: 'label',value: 'value',children: 'children'} |
className | 类名 | [String , Array ] |
- |
customStyle | 自定义样式 | Object |
- |
url | 动态获取数据url | String |
- |
method | 动态获取数据请求方式 | String | GET |
params | 动态获取数据请求参数 | Object |
- |
parseData | 解析接口返回的数据 | Function |
- |
searchable | 是否可远程搜索 | Boolean |
- |
searchKey | 搜索关键字字段名 | String |
label |
remoteMethod | 远程搜索方法 | Function |
- |
listeners | 事件 | Object |
{} |
基本实现
DynamicSelect/select.js
export default {
name: 'DynamicSelect',
props: {/* 暂时就上面那些 */},
inheritAttrs: false,
inject: {
jlFormSubject: { default: null } // 接受注入的表单实例
},
data() {
return {
// 默认事件
defaultOn: {
input: this.handleChange
},
optionsData: this.options,
loading: false,
oldOptionsData: null,
}
},
computed: {
// 因为Vue不能修改props传递过来的值, 所以借助新的值进行传递
newValue: {
get({ value }) {
return value
},
set(val) {
// 双向绑定
this.$emit('update:value', val)
return val
}
},
// 支持listener对象传入回调
onEvents() {
return Object.assign({}, this.defaultOn, this.listeners)
},
// props支持扩展,比如传入formatter函数进行格式化
optionsProps() {
return Object.assign({}, defaultProps, this.props)
},
bindAttrs() {
const obj = {
// 禁用的情况下不显示placeholder
placeholder: this.disabled ? '' : this.$attrs.placeholder || '请选择'
}
// 远程搜索功能开启再添加必要props
if (this.searchable) {
obj.filterable = true
obj.remote = true
}
return Object.assign(
{},
this.$attrs,
obj
)
},
// 组合远程搜索params: 尽可能增强容错性
requestOption() {
let paramsKey = this.method.toUpperCase() === 'GET' ? 'params' : 'data'
return {
baseUrl: this.$attrs.baseUrl,
url: this.url,
method: this.method
[paramsKey]: this.params || this.$attrs.data
}
}
},
watch: {
options: {
handler(value) {
this.optionsData = value
},
deep: true,
immediate: true
},
// 允许动态修改url、params、method
requestOption: {
handler() {
if (this.url) {
this.getOptionsData()
}
},
deep: true
}
},
}
以上哪些就是我们的动态数据源了,从上面那些大致可以get到封装组件技巧:
- 通过computed计算属性的
getter/setter
Or$emit
来实现v-model
- 通过computed将一个功能的几个属性整合起来通过watch侦听依赖数据的变动来作出行动(我们一般不会把关键性的props定义成对象让用户传递, 比如定义requestOption的props让用户传递对象的配置项)
- 对于太过边缘的属性不会一个个都用props去接,通过computed将$attrs和props根据逻辑进行整合成一个属性对象
实现动态请求数据及响应数据变动
// 都是接着上面来的,还是同一个文件
created() {
// 非级联select
if (!Reflect.has(this.bindAttrs, 'cascade') && this.url) {
// getOptionsData:获取数据后返回promise, 可以在获取数据后做其他操作
this.getOptionsData().then(data => {
// 比如自动选中第一个等等。。。
})
}
},
methods: {
// 动态拉取选项数据
async getOptionsData() {
return new Promise((resolve, reject) => {
let {
requestOption,
parseData
} = this
this.loading = true
// 再做一次兜底校验
if (!requestOption.url) return
request(...requestOption).then((res = {}) => {
this.loading = false
// 对于响应数据可以进行处理
/*
1. 比如:接口返回了这样的数据结构 {code: 200, pageData: {data: []}, pageIndex: 1, pageSize: 100,} => 可以通过parseData方法 return {data: res.pageData?.data || []}
2. 再比如:我们需要加个全部选项:return {data: res.pageData?.data ? [{label: '全部', id: 'all'}, ...res.pageData.data] : []}
*/
if (parseData && typeof parseData === 'function') {
res = parseData(res)
}
if (Array.isArray(res.data)) {
this.optionsData = res.data
} else {
this.optionsData = []
}
resolve(this.optionsData)
}).catch((e) => {
this.optionsData = []
this.loading = false
reject(e)
})
})
}
}
从上面那些大致可以get到封装组件技巧:
- 尽量多
spread
(…)运算符,避免写死key等 - 不太好控制的区块要尽可能将主权交给使用者,我们一般通过回调实现
render函数
下面通过render函数来渲染select
, 这里并没有用jsx的写法。这块如果不知道建议写去过下文档的render,深入数据对象那块。传送门👉深入数据对象
render(h) {
const self = this
// 渲染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
}]
})
return h('el-select', {
// 支持自定义className
class: self.className,
// 静态的className,可以通过这个来写点自样式
staticClass: 'jl-full-line',
// 也支持写权限较高的行内样式
style: self.customStyle,
// 将props全部传入,原生attrs支持就有效果
attrs: {
...self.bindAttrs
},
props: {
value: self.newValue,
loading: self.loading,
...self.bindAttrs
},
on: {
...self.onEvents, // 将所有listeners监听全部传入
input: self.handleInputEvent,
change: self.handleChangeEvent,
'visible-change': self.handleVisibleChange
},
}, optionsVnode)
}
从上面那些大致可以get到封装组件技巧:
spread
(…)运算符,让你组件属性大量可扩展
这里可能有人就会问了,为什么不用$listener
,非要自己通过回调实现自定义事件,意义何在?
如果你写过很多基础组件可能会遇到过
当这个组件当做其他封装组件的子组件的时候,其他组件也会通过
v-on=$listener
将监听的自定义事件透传下来,如果组件嵌套很多,组件内部的事件很有可能会被外部组件影响,特别:input, change
这类事件,当你组件自身监听过,外层通过透传也监听到了
对于上面监听的input、change
事件这里就不解析了,这里讲一点handleValueChanged
/*
对于value又是我们会需要进行foramtter,比如:
我们拉取的数据是这种结构:
data: [{id: '1', name: 'A'}, {id: '2', name: 'B'}, xxx]
我们多选之后,拿到的数据是这样的[{id: '1', name: 'A'}, {id: '2', name: 'B'}],但是后端要的数据结构是这种
[{projectName: 'A', projectEntrys: {id: '1', name: 'A'}, ... }]
*/
上面这种情况相信在项目中是常见的,我们肯定不能通过监听事件来变动value, formatter
就是最好的方式,根据当前业务去写逻辑就可以了,这里就不贴代码了
后续内容下个章节更新
- 通过自定义slots来渲染组件内容
- 提供组件外层render函数灵活渲染组件提供的作用域插槽
效果图💗
写在最后
如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下
我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。
往期文章
【前端体系】从一道面试题谈谈对EventLoop的理解(更新了四道进阶题的解析)
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 前端自学驿站 原文链接:https://juejin.im/post/6950974720015597604