react-resource-view
Create your own layout
One command writes the eighth layout: a single file, yours to draw.
The seven layouts that ship with the package answer the usual questions about a collection. The eighth is yours — a heatmap, a map, a gallery, whatever your records actually look like — and it is one file: three components and a layout.custom() call.
Rather than copy that file from this page, have the command write it.
One command#
npx react-resource-view create-view-variant Heatmap --dir src/viewsNothing is installed and nothing is configured: the command ships with the package, writes one file, and prints how to declare it. Run it bare and it asks for what it needs — the name, then where the file goes, offering the first of src/views, src/components or the current directory that exists:
npx react-resource-view create-view-variant| Name | Type | Default | Description |
|---|---|---|---|
name | argument | asked | What the layout switcher shows — Heatmap, "Kanban board". The file, the components and the type are all derived from it. |
--dir, -d | path | asked | Where the file goes. Created if it does not exist. |
--icon, -i | string | LayoutGrid | A lucide-react icon name for the switcher tab. |
--jsx | flag | — | Write JavaScript rather than TypeScript. |
--force, -f | flag | — | Overwrite the file if it is already there. |
--dry-run | flag | — | Print the file instead of writing it. |
--yes, -y | flag | — | Never ask: take the defaults for whatever was not passed. What a script wants. |
What it writes#
One file, importing nothing but the package and an icon. It runs as it lands — the point is that you then delete what it drew and draw your own:
// src/views/heatmapLayout.tsx — comments trimmed
import { Flame } from "lucide-react"
import {
ItemRender,
layout,
ListPagination,
ListResourceViewButton,
useLayout,
type ItemComponentPropsInterface,
type LayoutOptions,
type ListComponentPropsInterface,
type RowComponentPropsInterface,
} from "react-resource-view"
export interface HeatmapOptions {
dense?: boolean
}
/** One field of a record. */
export function HeatmapItem({ formInput }: ItemComponentPropsInterface) {
if (!formInput) return null
return <>{ItemRender(formInput.value)}</>
}
/** One record. */
export function HeatmapRow({ row }: RowComponentPropsInterface) {
const data = row?.data ?? {}
const fields = Object.entries(data).filter(
([key]) => !key.startsWith("@") && key !== "id"
)
return (
<div className="min-w-0 space-y-1">
{fields.map(([key, value]) => (
<div key={key} className="flex items-baseline gap-3 text-sm">
<span className="w-28 shrink-0 truncate text-muted-foreground">{key}</span>
<div className="min-w-0 [&_p]:mt-0">
<HeatmapItem formInput={{ name: key, value }} />
</div>
</div>
))}
</div>
)
}
/** The whole collection. */
export function HeatmapList({ rows = [] }: ListComponentPropsInterface) {
const { dense = false } = useLayout<HeatmapOptions>().options
return (
<div className="w-full">
<ul className={dense ? "space-y-1" : "space-y-3"}>
{rows.map((row, index) => (
<li key={"row-" + index} className="flex items-center gap-4">
<div className="min-w-0 flex-1">
<HeatmapRow row={row} />
</div>
<ListResourceViewButton data={row.data} />
</li>
))}
</ul>
<ListPagination />
</div>
)
}
export default function heatmapLayout(
{ dense = false, components, ...options }: LayoutOptions & HeatmapOptions = {}
) {
return layout.custom({
type: "heatmap",
label: "Heatmap",
icon: Flame,
...options,
components: {
list: HeatmapList,
row: HeatmapRow,
item: HeatmapItem,
...components,
},
options: { dense },
})
}The three slots#
A layout draws a list through three components, given in components:
listdraws the collection. It receivesrows— the current page, filtered and paginated.rowdraws one record. It receivesrow, whosedatais the record as the API answered it.itemdraws one field. It receivesformInput, andItemRenderis the package's own renderer for a value — booleans, relations, arrays and objects included.
Everything else is one hook away: useLayout gives the layout and its own options, useCurrentViewResourceContext the resource, the loading state, the filters, the selection and the view itself, and useList the rows with the mutations that go with them.
Declaring it#
A scaffolded layout is declared exactly like a built-in one — it is the same kind of object:
import { layout } from "react-resource-view"
import heatmapLayout from "./views/heatmapLayout"
form: articleForm,
views: {
list: {
layouts: [
heatmapLayout(), // the default: the first one listed
layout.table(),
],
},
}The list below runs a layout this command wrote. The file was not touched afterwards, which is why it draws every field it finds: switch to the table to see the same records through a layout that ships with the package.
A scaffolded layout, beside the table
Options of your own#
The generated function takes LayoutOptions — what every layout accepts: id, label, icon, fields, form, fullWidth, components — and whatever you add to its options interface. The dense option in the scaffold is there to be replaced:
// Its own options travel to the components…
layouts: [heatmapLayout({ dense: true, fields: ["title", "status"] })]
// …and are read back with useLayout(), inside any of the three.
const { dense = false } = useLayout<HeatmapOptions>().optionsThe type is the id
A layout is identified by its type, and that id is what travels in the URL — a new label breaks no shared link. Pass an id when the same layout appears twice in one layouts.
heatmapLayout() // id: "heatmap"
heatmapLayout({ label: "Compact" }) // id: "heatmap" still
heatmapLayout({ id: "compact", label: "Compact" }) // id: "compact"Without the command#
There is no registration step and no plugin: the command saves you the typing, nothing more. A layout written by hand is a layout.custom({ type, components }) call with the components you already have, and the layouts page covers what every layout shares with the seven built-in ones.