A grid of record cards to search, filter, sort, and page through.
import { RecordGrid, gridLayout } from '@oztix/roadie-components/record-grid'import { Records, useRecords } from '@oztix/roadie-components/records'import { recordFields } from '@oztix/roadie-core/records'
Card parts can hold functions (alt and cell), as can fields with a match, getRowId, and the useRecords instance, so render the grid in a client component, a file that starts with 'use client'.
Describe the records once as fields, then name in card the field each part of a card shows. Search, the sort in Configure, and paging come built in. Cards are at least 16rem wide, as many columns fit as the width allows, and a phone shows one.
Each part names a field: title names the record and carries its getRowHref link, description sits under it, leading beside it, and trailing at its end, or over the banner when there is one. image is a 16:9 banner from an image URL, with a neutral tile and an image icon for a record without one. details are label and value pairs under the title, in order, each value at the card's end. A part is a field's key, or { key, alt, cell } to give an image its alt text or replace how the value shows. Values read as their field does, so a status field shows a Badge.
Pass loading while records load and error when the load fails. With no records yet, loading shows still placeholder cards; with records, it dims them and runs a bar under the header. An error replaces the cards, and onRetry adds a Retry button. With no records, the empty state says whether none exist yet or none match, and offers to clear the search and filters.
Give Records both a table and a grid, and Configure shows a Layout switcher above the sort. The view's layout changes with it, so the URL and a saved view keep the layout, and the search, filters, and sort stay as they are. Each layout comes back with its settings as last shown, such as the table's hidden columns, or the saved view's when it was saved in that layout, so switching back reads as unchanged. Content shows the layout the view names, or the first one given.
Configure lists the fields a card shows as details. Drag a handle to reorder them, or click it or press Enter for a Move menu. The eye beside each one hides it. Hidden fields are listed apart, in the fields' own order, and showing one again adds it to the end. The fields the card places as its title, description, image, leading, or trailing aren't listed. When the grid defines details, the last one stays shown.
The fields write the view's layout.fields, the shown keys in order, left out once back to the grid's details, so equalRecordViews reads the view as unchanged. A view's keys for fields this grid doesn't have, such as one only some people see, are kept in their places.
Cards have no checkboxes, so a selectable grid offers Select mode, as a narrow table does. Select in the header shows a checkbox on each card, a tap anywhere on a card selects it rather than opening it, and the header offers Select all and Done. Escape or Done leaves the mode and clears the selection. The bulk actions float at the foot of the screen with the count. Switching to a table keeps the selection, with a checkbox on each row.
Past 100 records on a page, the grid renders only the rows of cards near the screen, measuring each row as it renders, so find in page and printing see only those. maxHeight or fill scrolls the cards in their own box. Give useRecords a loadRange and the grid loads records by range as people scroll, with placeholder cards for records still loading, a failed range's error and Retry in its place, and the first record on screen reported as position.row. A row set from outside, such as from the URL, scrolls into view at the row of cards that holds it. See Range loading for the loading side, which is the same for every layout.
layouts={[tableLayout(columns),gridLayout({ image: 'image', title: 'name', description: 'venue' })]}
Do
Offer the grid beside the table for events, venues, and products, where an image or a status at a glance helps people find the record.
<RecordGrid fields={orderFields} card={{ title: 'orderNumber', details: ['total', 'placedAt', 'email', 'status', 'tickets'] }} />
Don’t
Don't use a grid for records people compare by their values, such as orders. A table lines the values up in columns.
gridLayout({ title: 'name', description: 'venue', details: ['startsAt', 'sold'] })
Do
Show two or three details. People add more from Configure when they need them.
gridLayout({ title: 'name', details: ['venue', 'location', 'startsAt', 'endsAt', 'sold', 'capacity', 'gross'] })
Don’t
Don't list every field. Tall cards fit few to a screen and bury the title.
caption names the cards: they are a list named by the caption, each card an item with its place in the whole list (aria-posinset and aria-setsize) when the list holds only some of them, such as a page of several or more than 100 cards. Loading by range, the size is -1 until the total is known.getRowHref it is a link, and the whole card takes its hover and press; it opens in a new tab with Cmd or Ctrl, like any link.maxHeight or fill), the box is a region named "Events, scrolls" for a caption of Events.aria-busy while a range loads, placeholder cards are hidden from screen readers, and a failed range is announced. Records.Status announces Loading, the error, the result count, and the selection.Records as a grid of cards with the standard toolbar, pagination and status.
Every record, which Records filters, sorts and pages in the browser. With `rowCount`, the page the server returned. Leave it out with `loadRange`, which holds the records it loads.
How many records the search and filters match on the server. Giving it turns on server mode: `data` is one page, already searched, filtered, sorted and paged, and the search waits for a pause in typing. Fetch the `request` that `onRequestChange` hears. Once given, server mode stays: an `undefined` count keeps the last one. Pass 0 until the first count arrives. With `loadRange`, each range's result carries the count instead.
False when `rowCount` is a floor the server stopped counting at, such as a search index's 10,000: counts read "10,000+" and paging stops at the last page the server can reach. With `loadRange`, each range's result carries it instead.
Defaults to true.
What each record holds. Keep the array stable (module scope or `useMemo`).
Defaults to the record's index in `data`. Selecting in server or range mode needs it, as index ids repeat on every page and a refetch.
Gets the position too: a change to the search, filters or sort returns to the first page, and `onPositionChange` hears that first.
The saved or preset view the screen opened, so `modified` can tell when the view moves away from it and `resetView` can go back. Leave it out when no saved view is open.
Page, page size and, in range mode, the first row on screen: session state, never part of a view.
Defaults to { one: 'record', other: 'records' }.
Totals over every record the search and filters match, labelled "Totals for 1,234 records" by default. `true` or `'sum'` adds up the number and money fields in the browser, except shares, indexes and points. A server gives its own as `{ values }`, keyed by field, and `label`; keep passing the last ones while the next page loads. With `loadRange`, a range's result can carry them instead.
A fetch is under way. With no records the layout shows placeholders; with records it dims them.
The fetch failed. A string is shown as the message.
Shows a Retry button beside the error.
The viewer's IANA zone: it decides what "today" is and how timestamps read. Defaults to the browser's.
The moment relative dates resolve against. Defaults to when the list mounted in the browser: a server reads no clock, so a relative date filter shows the loading state until then. A new `now` that moves a relative date starts a range list over, so keep it stable.
Filters the page sets, such as the event a list of tickets belongs to. They filter like the view's but are never part of it, so they are never saved, cleared or edited: `Records.Search` shows them as locked chips. In server mode they reach the fetch through `scopedQuery`.
Lets people pick records for bulk actions.
Defaults to false.
Session state, like the position. A search or filter change drops picked records it hides.
Menu items for one record, such as `Menu.Item`s, shown in a menu on its row.
The record's page. Return `undefined` for a record with none.
Turns on range mode: one long list, searched, filtered and sorted on the server, that loads the records on screen as people scroll. Each gets the request for one range: one page of `pageSize` records from `start`, `end` exclusive. Resolve with its `rows` and, once known, `rowCount` (and `rowCountExact`). The list holds the rows for the request they answer, drops a reply for an older one, and starts over when the search, filters, sort, zone or a relative date's resolved dates change. Without a count, ranges load one after another until one comes back short. A rejection shows the range's error with Retry.
Hears `request` once the list is in the browser, then each time it changes: what to fetch in server mode, as the list resolved it.
The field each part of a card shows, as `gridLayout` takes them.
Accessible name for the cards.
Names the search field too, unless `searchLabel` is given.
Defaults to 'Search and filter'.
Names the search field when its placeholder doesn't.
The key that focuses the search, a `KeyboardEvent.key` value such as `/`, or `false` for none.
Defaults to '/'.
Defaults to [25, 50, 100].
Turns on selection and shows the bulk actions once something is selected.
Act on everything the search and filters match, with nothing selected, such as an export. Shown at the end of the toolbar.
Shows the open view's name, marked once it differs from `baseline`, with a menu to save, save as, rename, reset or delete it. The app keeps the views.
Scrolls the cards in their own box this tall, any CSS length.
Fills its parent's height and scrolls inside, like `maxHeight`. The parent needs a definite height, such as `Pane.Body`.