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 resource's form — the root form, shared by every view — in declaration order, skipping fields marked generatedValue. The header is the field's label, passed through the translation dictionary.
form: {
inputs: {
title: { label: "Title" }, // column 1
author: { label: "Author" }, // column 2
status: { label: "Status" }, // column 3
workspace: { generatedValue: true },// not a column
},
},
views: {
list: { layouts: [layout.table()] },
}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 layout its own fields — the columns, or the lines of a card, in order. A layout's form adjusts the fields themselves, merged over the resource's:
layouts: [
// The table shows everything…
layout.table(),
// …the cards, only what fits.
layout.cards({ columns: 3, fields: ["title", "status"] }),
]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 is drawn at once and written through the repository; a refusal puts the cell back.
Change a status in the table
Mark a field readonly to show it without letting it be edited from the list.
What a row offers#
At the end of every row sits a group of buttons. Which ones is a decision the list takes, because it depends on what the row already does — a table editing its cells in place has little use for a button whose only job is to show the same fields again, and three buttons are three buttons' worth of width taken from the columns.
views: {
list: {
// Read is not in the default — ["update", "delete"] is. Put it back where
// the detail view carries more than the row does.
rowActions: ["read", "update", "delete"],
},
}| Name | Type | Default | Description |
|---|---|---|---|
views.list.rowActions | ("read" | "update" | "delete")[] | ["update", "delete"] | Declared on the list view, or once for the application through configureResourceView({ defaults }). Order is the order the buttons are drawn in. |
Read is not in the default
It is one entry away — put it back where the detail view carries more than the list does, which is exactly the case a read form of its own describes. Permissions apply on top either way: an action listed here without the action refused by permissions renders nothing.
Rendering a cell yourself#
components.item 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 { ItemTable, layout } from "react-resource-view"
layout.table({
components: {
// One cell. `formInput` carries the field *and* its value for this row.
item: ({ formInput }) => {
if (formInput?.name !== "status") return <ItemTable formInput={formInput} />
return <StatusBadge value={formInput.value} />
},
},
})| Name | Type | Default | Description |
|---|---|---|---|
components.list | FC<{ rows, children }> | — | Draws the whole collection. The table or the grid itself. |
components.row | FC<{ row, children }> | — | Draws one record — a table row, a card. |
components.item | FC<{ formInput, children }> | — | Draws one field of one record — a cell. |
views.list.components.top / bottom | FC | — | Rendered above and below the list — a summary bar, a legend. |
views.list.components.empty | FC | — | Replaces the empty state. |
views.list.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#
layout.cards() takes one extra option, columns, the number of cards per row, from one to six. 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#
views.list.pageSize 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: {
list: { pageSize: 50 },
},
// The CSV export is offered by default; restrict it like any other action.
permissions: { export: () => isManager() },The export button is there unless permissions.export refuses it — on a resource with a path, whose dialect knows how to ask for one. 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.