react-resource-view
Table & cards
The two everyday layouts, column by column.
These two carry most lists. They read the same description and differ only in shape — which is why moving from one to the other is a one-line change, and why declaring both costs nothing.
Where the columns come from#
There is no columns array. The table renders one column per field of the view's form, in declaration order, skipping fields marked generatedValue. The header is the field's label, passed through the translation dictionary.
view: {
form: {
inputs: {
title: { label: "Title" }, // column 1
author: { label: "Author" }, // column 2
status: { label: "Status" }, // column 3
workspace: { generatedValue: true },// not a column
},
},
viewVariants: [tableViewOptionFactory()],
}One description, two readings
The consequence is worth stating plainly: adding a column adds an editable field, and marking a field required makes the create form require it. Nothing can drift, because there is only one description.
When the table wants more than the cards, give each variant its own form:
viewVariants: [
// The table shows everything…
tableViewOptionFactory({ form: fullForm }),
// …the cards, only what fits.
cardViewOptionFactory({ grid: 3, form: compactForm }),
]Editing in place#
A table cell is rendered by the same controller the form would use, so a row is editable where it stands — no modal, no separate edit mode. A change writes through the repository and the list refetches.
Change a status in the table
Mark a field readonly to show it without letting it be edited from the list.
Rendering a cell yourself#
itemComponent renders one field of one row. It receives the formInput — the field description carrying this row's value — so it can decide by field name and fall back to the default for the rest.
import { itemViewOptionFactory } from "react-resource-view"
tableViewOptionFactory({
// One cell. `formInput` carries the field *and* its value for this row.
itemComponent: ({ formInput }) => {
if (formInput?.name !== "status") return <ItemTable formInput={formInput} />
return <StatusBadge value={formInput.value} />
},
})| Name | Type | Default | Description |
|---|---|---|---|
listComponent | FC<{ rows, children }> | — | Draws the whole collection. The table or the grid itself. |
rowComponent | FC<{ row, children }> | — | Draws one record — a table row, a card. |
itemComponent | FC<{ formInput, children }> | — | Draws one field of one record — a cell. |
components.top / bottom | FC | — | Rendered above and below the list — a summary bar, a legend. |
components.noResult | FC | — | Replaces the empty state. |
components.pagination | FC | — | Replaces the pager. |
ItemRender is exported for the fallback: it turns a value into something readable — dates, booleans, IRIs — which is what the default cell uses.
Cards#
cardViewOptionFactory takes one extra option, grid, the number of cards per row. Four by default.
The same articles as cards
Cards read better than a table when:
- a record has a dominant field — a title, a name, an image;
- there are fewer than four attributes worth showing;
- the list is browsed rather than compared — comparison is what columns are for.
Pagination and export#
itemsPerPage sets the page size — thirty by default — and the current page travels in the URL alongside the filters, so a link to page four is a link to page four.
views: {
[ActionList.list]: {
// A CSV export, produced by the API, honouring the current filters.
behavior: { canExport: true },
},
}behavior.canExport adds an export button. The export is produced by the API in CSV and carries the filters currently applied, so what is downloaded is what is on screen.
Filters live next door
The filter bar above the list is an ordinary form with saveOnChange. It has its own page.