react-resource-view
Create your own variant
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 factory over createView.
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 id 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/heatmapViewFactory.tsx — comments trimmed
import { Flame } from "lucide-react"
import {
createView,
ItemRender,
ListPagination,
ListResourceViewButton,
useCurrentViewResourceContext,
type ItemComponentPropsInterface,
type ListComponentPropsInterface,
type RowComponentPropsInterface,
type ViewInterface,
} from "react-resource-view"
export interface HeatmapViewInterface extends ViewInterface {
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 view = useCurrentViewResourceContext().view as HeatmapViewInterface
const dense = view?.dense ?? false
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 heatmapViewFactory(
args?: Partial<HeatmapViewInterface>
): HeatmapViewInterface {
const defaultArgs: Partial<HeatmapViewInterface> = { dense: false, ...args }
return createView({
name: "Heatmap",
icon: Flame,
listComponent: HeatmapList,
rowComponent: HeatmapRow,
itemComponent: HeatmapItem,
...defaultArgs,
})
}The three slots#
A variant is a view, and a view renders through three components:
listComponentdraws the collection. It receivesrows— the current page, filtered and paginated.rowComponentdraws one record. It receivesrow, whosedatais the record as the API answered it.itemComponentdraws 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: useCurrentViewResourceContext gives the resource, the loading state, the filters, the selection and the view itself, and useList gives the rows with the mutations that go with them.
Declaring it#
A scaffolded variant is declared exactly like a built-in one — it is the same kind of object:
import { tableViewOptionFactory } from "react-resource-view"
import heatmapViewFactory from "./views/heatmapViewFactory"
view: {
form: articleForm,
viewVariants: [
heatmapViewFactory(), // the default: the first one listed
tableViewOptionFactory(),
],
}The list below runs a variant 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 interface extends ViewInterface, so a variant of yours takes both what a view accepts — form, itemsPerPage, behavior, components — and whatever you add to it. The dense option in the scaffold is there to be replaced:
// Anything the interface declares travels to the components…
viewVariants: [heatmapViewFactory({ dense: true, itemsPerPage: 50 })]
// …and is read back from the view, inside any of the three.
const view = useCurrentViewResourceContext().view as HeatmapViewInterface
const dense = view?.dense ?? falseThe name is the id
A variant is identified by the slug of its name, and that id is what travels in the URL. Pass a name to change it — which you have to do when the same factory appears twice in one viewVariants.
heatmapViewFactory() // id: "heatmap"
heatmapViewFactory({ name: "Compact" }) // id: "compact"Without the command#
There is no registration step and no plugin: the command saves you the typing, nothing more. A variant written by hand is a createView call with the components you already have, and the layouts page covers what every variant shares with the seven built-in ones.