我们每天有可能都在与数据列表打交道,比如列表的分页、查找列表(搜索查询)、按照指定的列升序降序排列这些需求,你可能再尝试使用 react-table or Ant Design table 这样的组件完成这些需求,但通常这些库与你特定案例的设计和需求不匹配,并且具有许多你不需要的功能,有时,自己动手可能会更好些,以便在功能和设计方面具有完全的灵活性。今天小编看到一位国外大佬写的关于此主题的文章,在此分享给大家,本篇文章并不是完全按照原作者的文章进行翻译,加上了小编的一些理解,希望对大家有所帮助。
在列表读取方面,由于数据量大的原因我们一般都是通过接口 的方式获取数据,但是有时候在数据量不多的情况,我们完全可以将数据一次性获取,在前端处理相关的分页、查找、排序的需求。
本案例将使用 React 进行介绍(更多讲解其实现的原理和步骤,你可以用其他框架进行实现),具体列表如下图所示,有姓名、年龄、是否经理人、入职日期这几列,我们可以在各列表头下面的输入框进行模糊搜索内容,同时表头旁边有上下箭头操作可以按照具体的某列进行升序和降序排列,最后列表的最下方有个分页组件,我们可以进行分页的操作。
开始之前,我们在来总结下项目的需求:
本案例不会借助其他的第三方组库(除了基础的React),我们从 0 到 1 开始构建我们的列表组件。
在做案例前,我们先准备基础的数据方已便于演示,如下所示,包含了字符串、数据、布尔值、日期这几种类型的数据。
const rows = [ { id: 1, name: 'Liz Lemon', age: 36, is_manager: true, start_date: '02-28-1999' }, { id: 2, name: 'Jack Donaghy', age: 40, is_manager: true, start_date: '03-05-1997' }, { id: 3, name: 'Tracy Morgan', age: 39, is_manager: false, start_date: '07-12-2002' }, { id: 4, name: 'Jenna Maroney', age: 40, is_manager: false, start_date: '02-28-1999' }, { id: 5, name: 'Kenneth Parcell', age: Infinity, is_manager: false, start_date: '01-01-1970' }, { id: 6, name: 'Pete Hornberger', age: null, is_manager: true, start_date: '04-01-2000' }, { id: 7, name: 'Frank Rossitano', age: 36, is_manager: false, start_date: null }, { id: 8, name: null, age: null, is_manager: null, start_date: null }, ]
为了让表头和基础数据的关联,我们可以按照如下思路设计表头字段数据,如下所示:
const columns = [ { accessor: 'name', label: 'Name' }, { accessor: 'age', label: 'Age' }, { accessor: 'is_manager', label: 'Manager', format: value => (value ? '✔️' : '✖️') }, { accessor: 'start_date', label: 'Start Date' }, ]
你可能注意到了,我们的表头属性和列表数据的属性有相关性,我们可以用表头的属性方便在行里进行遍历循环显示数据,同时我们增加了一个格式化的属性,我们可以按照自己的需求自定义数据项的显示格式(这里我只是处理了布尔值的自定义格式化,有兴趣的话你可以尝试下日期的格式化)
我更喜欢在数组map函数里使用 return,这更方便我进行编辑和调试
基于上面的数据,我们来渲染 table.js 组件,示例代码如下:
//table.js const Table = ({ columns, rows }) => { return ( <table> <thead> <tr> {columns.map(column => { return <th key={column.accessor}>{column.label}</th> })} </tr> </thead> <tbody> {rows.map(row => { return ( <tr key={row.id}> {columns.map(column => { if (column.format) { return <td key={column.accessor}>{column.format(row[column.accessor])}</td> } return <td key={column.accessor}>{row[column.accessor]}</td> })} </tr> ) })} </tbody> </table> ) }
这里请注意 key 值的正确使用
接下来,将数据传递到我们的表格组件里。
<Table rows={rows} columns={columns} />
初次渲染,我们的表格是这样的效果:
到这里,我们将基础表格构建出来了,接下来继续添加分页的功能。
我们可以有很多方式在前端设置分页。
例如下图谷歌界面的分页方式,显示上一页和下一页的按钮,以及当前的页面和前后相关的页面,我们可以进行相关的操作。
就我个人而言,我更喜欢 “第一页 ️️️⏮️”,“上一页⬅️”,“下一页 ➡️
” 以及“最后一页⏭️”的分页操作,如果当前页没有上一页或下一页的操作时,我们应该隐藏或者禁止相关按钮的点击。
在这个列表组件里,我们的分页将实现这些需求:
改写后的 Table.js 文件如下:
//table.js const Table = ({ columns, rows }) => { const [activePage, setActivePage] = useState(1) const rowsPerPage = 3 const count = rows.length const totalPages = Math.ceil(count / rowsPerPage) const calculatedRows = rows.slice((activePage - 1) * rowsPerPage, activePage * rowsPerPage) /* ... */ return ( <> <table>{/* ... */}</table> <Pagination activePage={activePage} count={count} rowsPerPage={rowsPerPage} totalPages={totalPages} setActivePage={setActivePage} /> </> ) }
从上述代码,你可能注意到了 calculatedRows 这个变量,根据当前页、每页的行数,用于计算当前页显示哪些数据(这是数据分页的关键),这里我们使用了数组的 slice 方法用来截取数组。
分页组件的代码,如下所示 Pagination.js:
// Pagination.js const Pagination = ({ activePage, count, rowsPerPage, totalPages, setActivePage }) => { return ( <div className="pagination"> <button>⏮️ First</button> <button>⬅️ Previous</button> <button>Next ➡️</button> <button>Last ⏭️</button> </div> ) }
接下来我们继续看看分页后的效果:
接下来我们继续定义分页按钮相关的事件,进行页面的切换,同时下面的文本显示当前的页面和相关的数据,完善后的 Pagination.js 示例代码如下:
const Pagination = ({ activePage, count, rowsPerPage, totalPages, setActivePage }) => { const beginning = activePage === 1 ? 1 : rowsPerPage * (activePage - 1) + 1 const end = activePage === totalPages ? count : beginning + rowsPerPage - 1 return ( <> <div className="pagination"> <button disabled={activePage === 1} onClick={() => setActivePage(1)}> ⏮️ First </button> <button disabled={activePage === 1} onClick={() => setActivePage(activePage - 1)}> ⬅️ Previous </button> <button disabled={activePage === totalPages} onClick={() => setActivePage(activePage + 1)}> Next ➡️ </button> <button disabled={activePage === totalPages} onClick={() => setActivePage(totalPages)}> Last ⏭️ </button> </div> <p> Page {activePage} of {totalPages} </p> <p> Rows: {beginning === end ? end : `${beginning} - ${end}`} of {count} </p> </> ) }
这是分页最基础的功能,你可以在此基础上,根据自己组件的需求,去完善此分页组件的样式。
接下来,我们需要完成列表的查找功能,每一列都支持数据查找,比如在姓名一列,我们输入 enn 将会匹配 Jenna Maroney 和 Kenneth Parcell 这两条数据。
我们需要创建一个搜索对象,用来分别存储搜索的键(列名)和对应值(输入框的值),由于支持多属性键值,可以支持多个列的复合查找。
每次搜索,我们都会重新将当前页面更新到第一页,数据量比较少,只是在这个案例中,查找显示分页就没太大的意义,这里我们先禁用。
接下来我们定义 filteredRows 变量和相关的方法,用来筛选出查找出来数据内容,同时将filteredRows 的长度赋值给 count 变量。
const Table = ({ columns, rows }) => { const [activePage, setActivePage] = useState(1) const [filters, setFilters] = useState({}) const rowsPerPage = 3 const filteredRows = filterRows(rows, filters) const calculatedRows = filteredRows.slice( (activePage - 1) * rowsPerPage, activePage * rowsPerPage ) const count = filteredRows.length const totalPages = Math.ceil(count / rowsPerPage) }
我们的查找要根据数据的类型,比如字符串、数字、布尔值的查找逻辑是不一样的,相关逻辑如下:
function filterRows(rows, filters) { if (isEmpty(filters)) return rows return rows.filter(row => { return Object.keys(filters).every(accessor => { const value = row[accessor] const searchValue = filters[accessor] if (isString(value)) { return toLower(value).includes(toLower(searchValue)) } if (isBoolean(value)) { return (searchValue === 'true' && value) || (searchValue === 'false' && !value) } if (isNumber(value)) { return value == searchValue } return false }) }) }
这里的
isString
,isBoolean
等是我自定义的工具函数,用来判断数据类型的
你也许注意到了,这个案例我只是用输入框进行数据的查找,其实你可以进行完善,比如是否经理人用个下拉列表,日期选择可以用个日历插件等给用户一个好的用户体验,这个案例只是给大家描述下基础的思路。
如果用户在输入框里输入了任何内容,我们需要将其添加到我们定义的搜索对象里,如果用户将输入项删除,我们还需要将其搜索对象的属性Key值进行删除,具体的输入框的查找事件定义如下:
const handleSearch = (value, accessor) => { setActivePage(1) if (value) { setFilters(prevFilters => ({ ...prevFilters, [accessor]: value, })) } else { setFilters(prevFilters => { const updatedFilters = { ...prevFilters } delete updatedFilters[accessor] return updatedFilters }) } }
接下来我们来修改 table.js 组件文件,将查找逻辑添加进行,修改后的代码如下:
// table.js <thead> <tr>{/* ... */}</tr> <tr> {columns.map(column => { return ( <th> <input key={`${column.accessor}-search`} type="search" placeholder={`Search ${column.label}`} value={filters[column.accessor]} onChange={event => handleSearch(event.target.value, column.accessor)} /> </th> ) })} </tr> </thead>
最后我们来完成最后一个功能,让表格支持排序功能:
以下表格,是针对不同类型的数据的升序和降序排列的总结,方便大家理解:
本示例只展示了按照单列的逻辑进行升序或降序,只要单击任意一列的排序,就会将其他列恢复为默认的不排序规则,如果想支持多列的复合排序,你可以继续完善本案例。
为了支持排序,我们需要定义两个数据状态用来支持排序:
完善后的 table.js 组件代码如下:
const Table = ({ columns, rows }) => { const [activePage, setActivePage] = useState(1) const [filters, setFilters] = useState({}) const [sort, setSort] = useState({ order: 'asc', orderBy: 'id' }) // ... }
接下来定义排序事件,这里我们使用 localeCompare 函数来分别处理字符串、数字、数据类型 :
function sortRows(rows, sort) { return rows.sort((a, b) => { const { order, orderBy } = sort if (isNil(a[orderBy])) return 1 if (isNil(b[orderBy])) return -1 const aLocale = convertType(a[orderBy]) const bLocale = convertType(b[orderBy]) if (order === 'asc') { return aLocale.localeCompare(bLocale, 'en', { numeric: isNumber(b[orderBy]) }) } else { return bLocale.localeCompare(aLocale, 'en', { numeric: isNumber(a[orderBy]) }) } }) }
点击表头的排序按钮(⬆️、⬇️、↕️)排序将会触发重新分页,同时还要判断当前的排序状态,如果当前是升序,则将其更改为降序
const handleSort = accessor => { setActivePage(1) setSort(prevSort => ({ order: prevSort.order === 'asc' && prevSort.orderBy === accessor ? 'desc' : 'asc', orderBy: accessor, })) }
我们继续处理表头的排序按钮展示,用来触发排序事件,同时用来显示当前的排序是按照具体的哪一数据项排序的,完善后的 table.js 组件代码如下所示:
<thead> <tr> {columns.map(column => { const sortIcon = () => { if (column.accessor === sort.orderBy) { if (sort.order === 'asc') { return '⬆️' } return '⬇️' } else { return '️↕️' } } return ( <th key={column.accessor}> <span>{column.label}</span> <button onClick={() => handleSort(column.accessor)}>{sortIcon()}</button> </th> ) })} </tr> <tr>{/* ... */}</tr> </thead>
最后我们 table.js 的关键部分的代码就完成了,你可以进行查询、排序、分页等。
export const Table = ({ columns, rows }) => { const [activePage, setActivePage] = useState(1) const [filters, setFilters] = useState({}) const [sort, setSort] = useState({ order: 'asc', orderBy: 'id' }) const rowsPerPage = 3 const filteredRows = useMemo(() => filterRows(rows, filters), [rows, filters]) const sortedRows = useMemo(() => sortRows(filteredRows, sort), [filteredRows, sort]) const calculatedRows = paginateRows(sortedRows, activePage, rowsPerPage) // ... }
你可能注意到上述代码,我们将排序和查找操作放到 useMemo HOOK 函数里提升性能(类似 vue 框架的计算属性 computed)
到此,我们的表格组件就完成了,你可以进行排序、分页、查找,实在太棒了!
祝贺你能看到这里,终于可以松口气了,我们再不借助任何第三方库的情况下完成了列表的分页、排序、查找,是不是很不错,既然已经完成了基础的功能,我们可以在此基础去继续改进它,让它变的更复杂、更好、更强大!
接下来你可以这样继续改进它:
如过你想体验本案例及获取本案例的源码,请复制链接 https://codesandbox.io/s/sorting-filtering-pagination-de7v3?file=/src/Table.js 或 文末的阅读原文进行体验,感谢你的阅读。
参考文章:https://www.taniarascia.com/front-end-tables-sort-filter-paginate/ 作者:Tania
注:本文属于原创文章,版权属于「前端达人」公众号及 qianduandaren.com 所有,未经授权,谢绝一切形式的转载