React - 实现一个基于 Antd 的 Select 下拉懒加载组件
前言
最近的开发迭代业主需要用到一个 Select 下拉加载功能,但项目组上没有这个组件,Antd Design官网也没有提供现成的,查阅资料也没有发现符合业务的组件,特此自己造轮子封装。
使用场景
注:当下拉框数据较多时,后端采用分页搜索模式,这个时候就可以使用这个组件。
实现思路
想要封装一个好的组件,我们需要考虑它的共用性,既然是下拉加载组件,它应具备以下功能:
- 应该继承 Select 组件的全部属性
- 滚动到底部,数据加载下一页,如果接口返回的条数小于当前设置的条数,就取消滚动加载功能
- 支持自定义 options ...
搭建基本结构
我们先看下后端返回的数据结构:
如果数据结构不一样,可根据后端要求修改:
import { useBoolean, useRequest, useSetState } from 'ahooks'
import { Select, Spin } from 'antd'
import React, { FC, useState } from 'react'
import { Res } from '@/interface/api';
import { ComponentProps, PagetionProps } from './interface'
const { Option } = Select;
const LazyLoadSelect: FC<ComponentProps> = ({
apiUrl,
pageRows = 15,
resultField = 'list',
fieldNames = {
label: 'id',
value: 'name',
},
emptyText = '没有更多了',
manual = false,
...props
}) => {
// 默认分页参数
const [requestParams, setRequestParams] = useSetState<PagetionProps>({
currentPage: 1,
pageRows,
searchKey: '',
})
// 是否还有更多数据
const [isEmptyData, { setTrue: setEmptyDataTrue, setFalse: setEmptyDataFalse }] = useBoolean(false)
// 请求的数据列表
const [requestList, setRequestList] = useState([])
/**
* @description: 获取请求数据
* @author: Cyan
*/
const { loading: requestLoading, runAsync: runAsyncRequestList } = useRequest(
async (params: PagetionProps): Promise<Res> => await apiUrl(params),
{
manual,
defaultParams: [{ ...requestParams }],
onSuccess: (res: Res) => {
if (res.code === 200) {
// 获取列表数据
const result = res?.data?.[resultField] || []
setRequestList([...requestList, ...result])
// 当返回条数小于分页条数,或者没有返回数据就代表没有更多数据
if (result?.length < requestParams.pageRows) {
setEmptyDataTrue()
}
}
},
},
);
return (
<Select
showSearch
loading={requestLoading}
filterOption={false}
placeholder="请选择"
{...props}
>
{
requestList?.map((item) => {
return (
<Option
{...item}
value={item[fieldNames.value]}
key={item[fieldNames.value]}
>
{item[fieldNames.label]}
</Option>
)
})
}
{/* 没有更多数据 */}
{
isEmptyData &&
<Option disabled>
<div style={{ color: '#bfbfbf', textAlign: 'center', fontSize: 12, pointerEvents: 'none' }}>{emptyText}</div>
</Option>
}
{/* 下拉加载按钮 */}
{
requestLoading &&
<Option disabled>
<div style={{ textAlign: 'center', pointerEvents: 'none' }}>
<Spin size='small' />
</div>
</Option>
}
</Select>
)
}
export default LazyLoadSelect
这里我使用了 ahooks库
完善功能
架子我们已经搭好了,现在在其添砖加瓦。
- 判断滚动距离,滚动到底部加载下一页
这里我们用到了 Select组件 的
onPopupScroll
API
// 判断是否滚动到底部
const { run: onPopupScroll } = useDebounceFn(
async (e) => {
e.persist();
const { scrollTop, offsetHeight, scrollHeight } = e.target;
// 距离底部多少距离开始加载下一页
if (((scrollTop + offsetHeight) === scrollHeight) && !isEmptyData) {
// 当滚动到底部时,我们下载下一页,同时更新分页状态
setRequestParams({ currentPage: requestParams.currentPage + 1 })
await runAsyncRequestList({ ...requestParams, currentPage: requestParams.currentPage + 1 })
}
},
{ wait: 350 },
);
<Select
showSearch
loading={requestLoading}
filterOption={false}
onPopupScroll={(e) => onPopupScroll(e)}
onSearch={(value: string) => handleSearch(value)}
placeholder="请选择"
onClear={handlerClear}
{...props}
>
</Select>
- 增加搜索功能 组件支持搜索功能,当用户搜索后重新触发接口加载,并重置分页状态,这里还要加一个防抖函数,避免输入过程中频繁触发接口请求:
// 重新请求
const initRequest = async (newValue: string) => {
// 搜索重置分页
setRequestParams({ currentPage: 1, searchKey: newValue })
setEmptyDataFalse()
// 重置数据列表
setRequestList([])
await runAsyncRequestList({ ...requestParams, ...extraParams, currentPage: 1, searchKey: newValue })
}
// 搜索回调
const { run: handleSearch } = useDebounceFn(
async (newValue: string) => {
// 重置参数,重新请求
initRequest(newValue)
},
{ wait: 500 },
);
- 增加清空数据回调 当用户搜索的数据只有一条时,选中后清空数据,这时候我们应该重新请求数据:
// 清除内容时的回调
const handlerClear = async () => {
// 当数据不足一页时,重新加载
if (requestList.length < requestParams.pageRows) {
initRequest('')
}
}
- 自定义 options
有时候需求需要自定义内容,如下图:
这时我们还要提供一个 props 给组件自定义,并提供当前的列表数据:
// 父组件
// 自定义 options
const renderOptions = (ownerHouseList: OwnerHouseProps[]) => {
return (
ownerHouseList?.map((house) => {
return (
<Option
value={house.id}
label={`${house.address}(${house.houseCode})`}
disabled={house.optional === 0}
key={house.id}
>
<Row>
<Col span={24}>{house.houseCode}</Col>
<Col span={24}>盘源地址:{house.address}</Col>
</Row>
</Option>
)
})
)
}
// 子组件
{
customizeOptions ? customizeOptions(requestList) :
requestList?.map((item) => {
return (
<Option
{...item}
value={item[fieldNames.value]}
key={item[fieldNames.value]}
>
{item[fieldNames.label]}
</Option>
)
})
}
- 细节完善
作为开发者,我们还需要考虑组件的共用性:
- 接口可能返回的字段不一样
- 下拉滚动到底部多少距离触发
- 一些自定义文案
- 父组件需要调用子组件的请求方法 ...
最终我们的代码如下:
/*
* @Description: Select 下拉懒加载组件
* @Version: 2.0
* @Author: Cyan
* @Date: 2023-03-10 10:04:17
* @LastEditors: Cyan
* @LastEditTime: 2023-03-10 16:17:13
*/
import { useBoolean, useDebounceFn, useRequest, useSetState } from 'ahooks'
import { Select, Spin } from 'antd'
import React, { FC, useImperativeHandle, useState } from 'react'
import { Res } from '@/interface/api';
import { ComponentProps, PagetionProps } from './interface'
const { Option } = Select;
const LazyLoadSelect: FC<ComponentProps> = ({
apiUrl,
pageRows = 15,
resultField = 'list',
fieldNames = {
label: 'id',
value: 'name',
},
emptyText = '没有更多了',
onRef,
extraParams = {},
manual = false,
distanceBottom = 0,
customizeOptions,
...props
}) => {
// 默认分页参数
const [requestParams, setRequestParams] = useSetState<PagetionProps>({
currentPage: 1,
pageRows,
searchKey: '',
})
// 是否还有更多数据
const [isEmptyData, { setTrue: setEmptyDataTrue, setFalse: setEmptyDataFalse }] = useBoolean(false)
// 请求的数据列表
const [requestList, setRequestList] = useState([])
/**
* @description: 业主当事盘源列表
* @author: Cyan
*/
const { loading: requestLoading, runAsync: runAsyncRequestList } = useRequest(
async (params: PagetionProps): Promise<Res> => await apiUrl(params),
{
manual,
defaultParams: [{ ...requestParams, ...extraParams }],
onSuccess: (res: Res) => {
if (res.code === 200) {
// 获取列表数据
const result = res?.data?.[resultField] || []
setRequestList([...requestList, ...result])
// 当返回条数小于分页条数,或者没有返回数据就代表没有更多数据
if (result?.length < requestParams.pageRows) {
setEmptyDataTrue()
}
}
},
},
);
// 判断是否滚动到底部
const { run: onPopupScroll } = useDebounceFn(
async (e) => {
e.persist();
const { scrollTop, offsetHeight, scrollHeight } = e.target;
// 距离底部多少距离开始加载下一页
if (((scrollTop + offsetHeight + distanceBottom) >= scrollHeight) && !isEmptyData) {
setRequestParams({ currentPage: requestParams.currentPage + 1 })
await runAsyncRequestList({ ...requestParams, ...extraParams, currentPage: requestParams.currentPage + 1 })
}
},
{ wait: 350 },
);
// 重新请求
const initRequest = async (newValue: string) => {
// 搜索重置分页
setRequestParams({ currentPage: 1, searchKey: newValue })
setEmptyDataFalse()
// 重置数据列表
setRequestList([])
await runAsyncRequestList({ ...requestParams, ...extraParams, currentPage: 1, searchKey: newValue })
}
// 搜索回调
const { run: handleSearch } = useDebounceFn(
async (newValue: string) => {
// 重置参数,重新请求
initRequest(newValue)
},
{ wait: 500 },
);
// 清除内容时的回调
const handlerClear = async () => {
// 当数据不足一页时,重新加载
if (requestList.length < requestParams.pageRows) {
initRequest('')
}
}
// 用 useImperativeHandle 暴露一些外部ref能访问的属性
useImperativeHandle(onRef, () => ({ runAsyncRequestList, requestParams }))
return (
<Select
showSearch
loading={requestLoading}
filterOption={false}
onPopupScroll={(e) => onPopupScroll(e)}
onSearch={(value: string) => handleSearch(value)}
placeholder="请选择"
onClear={handlerClear}
{...props}
>
{
customizeOptions ? customizeOptions(requestList) :
requestList?.map((item) => {
return (
<Option
{...item}
value={item[fieldNames.value]}
key={item[fieldNames.value]}
>
{item[fieldNames.label]}
</Option>
)
})
}
{/* 没有更多数据 */}
{
isEmptyData &&
<Option disabled>
<div style={{ color: '#bfbfbf', textAlign: 'center', fontSize: 12, pointerEvents: 'none' }}>{emptyText}</div>
</Option>
}
{/* 下拉加载按钮 */}
{
requestLoading &&
<Option disabled>
<div style={{ textAlign: 'center', pointerEvents: 'none' }}>
<Spin size='small' />
</div>
</Option>
}
</Select>
)
}
export default LazyLoadSelect
使用方式
import LazyLoadSelect from '@/components/LazyLoadSelect'
<LazyLoadSelect
key="LazyLoadSelect"
onRef={lazyLoadSelectRef}
allowClear
placeholder="请搜索或选择经纪人姓名"
apiUrl={getBrokerInfoListForReport}
fieldNames={{ value: 'userId', label: 'userName' }}
onChange={onChangeUserName}
getPopupContainer={() => document.getElementById('black-container')}
/>
参数说明
参数 | 说明 | 类型 | 默认值 | 是否必传 |
---|---|---|---|---|
apiUrl | 请求接口 | Promise | - | √ |
pageRows | 每页显示条数 | number | 15 | - |
resultField | 后端返回数据字段 | string | list | - |
emptyText | 空数据的显示文案 | string | 没有更多了 | - |
onRef | 绑定子组件实例 | React.RefObject | - | - |
extraParams | 额外参数 | object | {} | - |
manual | 是否手动执行 | boolean | false | - |
distanceBottom | 滚动距离多少触发加载,默认滚动到底部,也就是0 | number | 0 | - |
customizeOptions | 自定义 option | - | - | - |
SelectProps | Antd Select选择器 Props,配置项 | - | - | - |
注意事项
-
如果接口请求后,下拉框没看到数据,请检查参数
fieldNames
是否配置正确。 -
如果下拉过程出现数据重复等BUG,请检查后端返回的数据
fieldNames.value
是否唯一,且必传,否则组件会出现BUG。 -
如果需要自定义
option
,请使用customizeOptions
属性 -
如果组件的功能不符合您的业务场景,请联系我拓展完善。
让我们看看最终效果:
如果此文章对你有帮助,请帮我点个赞吧!
转载自:https://juejin.cn/post/7208839038017798181