useList

Stable v1.0.0

A reactive search + per-field filter + sort + paginate pipeline for a plain array ref — wired 1:1 to the Table and Pagination component props/emits.

1. Interactive Playground (Orders)

One useList(orders, options) call drives every control below — search, the status/date/amount filters, column sorting, and pagination all read and write the same reactive state.

Status…
Order ID
Customer
Status
Total
Placed
ORD-0001
Marcus Chen
Processing
$52.99
Jul 28, 2026
ORD-0002
Emily Thorne
Shipped
$66.99
Jul 28, 2026
ORD-0003
John Doe
Delivered
$79.99
Jul 28, 2026
ORD-0004
Alicia Keys
Cancelled
$92.99
Jul 27, 2026
ORD-0005
Robert Baratheon
Pending
$106.99
Jul 27, 2026
ORD-0006
Nadia Ibrahim
Processing
$119.99
Jul 27, 2026
ORD-0007
Wei Zhang
Shipped
$133.99
Jul 26, 2026
ORD-0008
Sarah Jenkins
Delivered
$146.99
Jul 26, 2026

1 to 8 of 64 records

8 / page
Go to
Playground Integration
<script setup>
import { useList } from '@midnight-owl/use-list'

const orders = ref<Order[]>([...])

const {
  globalSearch,
  filters,
  sort,
  currentPage,
  rowsPerPage,
  paginatedData,
  totalRecords,
  handleSortChange
} = useList(orders, { searchFields: ['id', 'customer'], itemsPerPage: 8 })
</script>

<template>
  <InputText v-model="globalSearch" iconStart="lucide:search" placeholder="Search orders…" />

  <Table
    :records="paginatedData"
    :columns="orderColumns"
    :sortKey="sort.key"
    :sortDirection="sort.direction"
    @sort-change="handleSortChange"
  >
    <template #pagination>
      <Pagination
        v-model:currentPage="currentPage"
        v-model:rowsPerPage="rowsPerPage"
        :totalRecords="totalRecords"
      />
    </template>
  </Table>
</template>

globalSearch deep-searches every value in every record by default. Pass searchFields to constrain the scan — useful once records carry fields you don't want a stray keyword match to hit.

Constraining the search scope
// Deep-searches every value in every record by default
const { globalSearch } = useList(orders)

// Constrain the scan to specific fields (dot-notation for nested keys)
const { globalSearch } = useList(orders, { searchFields: ['id', 'customer'] })

3. Filtering

filters accepts three value shapes per key, matched automatically by useList — each one binds straight to the form component that already produces that shape, no adapter code needed.

String filter
<!-- filters.value.status is a plain string filter — matches via a
     case-insensitive "includes", so Select's exact option value just works -->
<Select v-model="filters.status" :options="statusOptions" placeholder="Status…" />
Range filter — { start, end }
<!-- RangeFilterValue is { start, end } — the exact shape DatePicker's
     mode="range" v-model already emits, so it binds directly, no adapter -->
<DatePicker v-model="filters.createdAt" mode="range" placeholder="Placed…" />
Min/max filter — { min, max }
const totalMin = ref<number | null>(null)
const totalMax = ref<number | null>(null)

// MinMaxFilterValue is { min, max } — pair two InputNumbers into one filter key
watch([totalMin, totalMax], ([min, max]) => {
  filters.value.total = { min, max }
})

4. Sorting

sort and handleSortChange line up 1:1 with Table's sortKey / sortDirection props and its sort-change emit.

Wiring sort to Table
<!-- sort/handleSortChange line up 1:1 with Table's sortKey/sortDirection
     props and its sort-change emit — no adapter code in between -->
<Table
  :records="paginatedData"
  :columns="orderColumns"
  :sortKey="sort.key"
  :sortDirection="sort.direction"
  @sort-change="handleSortChange"
/>

5. Pagination

currentPage, rowsPerPage and totalRecords line up 1:1 with Pagination's own v-models and prop. Set paginate: false when a server is doing the slicing instead of a local array.

Wiring pagination to Pagination
<!-- currentPage/rowsPerPage/totalRecords line up 1:1 with Pagination's
     v-model:currentPage, v-model:rowsPerPage and totalRecords prop -->
<Pagination
  v-model:currentPage="currentPage"
  v-model:rowsPerPage="rowsPerPage"
  :totalRecords="totalRecords"
/>
Headless mode (server-side pagination)
// Driving a server-paginated API instead of slicing a local array?
// Disable slicing and read sortedData/filteredData — currentPage and
// rowsPerPage stay reactive so you can watch them and trigger your own fetch.
const { sortedData, currentPage, rowsPerPage } = useList(orders, { paginate: false })

watch([currentPage, rowsPerPage], ([page, limit]) => {
  fetchOrders({ page, limit })
})

API Reference

Complete specifications for useList's options and returned state.

Options — useList(data, options)

Name
Type
Default
Description
searchFields
(keyof T | string)[]
undefined
Constrain globalSearch to these field paths (dot-notation for nested keys). Omit to deep-search every value in each record.
itemsPerPage
number
10
Initial rowsPerPage value, and what reset()/resetPagination() restore it to.
paginate
boolean | Ref<boolean>
true
Set to false (or a ref) to disable slicing and always return the full sorted/filtered set — for headless/server-driven pagination.

Return Values

Name
Type
Default
Description
globalSearch
Ref<string>
''
v-model target for a free-text search box.
filters
Ref<Record<string, ListFilterValue>>
{}
Per-field filter bag, keyed by field path. Bind a value directly (string filter), or { start, end } / { min, max } for range/min-max filters.
sort
Ref<{ key: string, direction: SortDirection }>
{ key: '', direction: null }
Current sort state. direction is 'asc' | 'desc' | null.
currentPage
Ref<number>
1
v-model:currentPage target for Pagination.
rowsPerPage
Ref<number>
itemsPerPage
v-model:rowsPerPage target for Pagination.
filteredData
ComputedRef<T[]>
-
Source data after globalSearch and filters are applied, before sorting.
sortedData
ComputedRef<T[]>
-
filteredData after sort is applied, before pagination — read this directly in headless/server-driven mode.
paginatedData
ComputedRef<T[]>
-
sortedData sliced to the current page. Equal to sortedData when paginate is false — pass straight to Table's records prop.
totalRecords
ComputedRef<number>
-
Count of sortedData — pass straight to Pagination's totalRecords prop.
totalPages
ComputedRef<number>
-
Total page count at the current rowsPerPage, minimum 1.
handleSortChange
(config: SortState) => void
-
Assign directly to Table's @sort-change — also resets currentPage to 1.
handleFilterChange
(key: string, value: ListFilterValue) => void
-
Sets a single filter key immutably. Optional — writing filters.value[key] directly works just as well.
reset
() => void
-
Clears globalSearch, filters and sort, and resets pagination to page 1 / itemsPerPage.
resetPagination
() => void
-
Resets only currentPage and rowsPerPage, leaving search/filters/sort untouched.