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/views

Nothing 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
NameTypeDefaultDescription
nameargumentaskedWhat the layout switcher shows — Heatmap, "Kanban board". The file, the components and the type are all derived from it.
--dir, -dpathaskedWhere the file goes. Created if it does not exist.
--icon, -istringLayoutGridA lucide-react icon name for the switcher tab.
--jsxflag—Write JavaScript rather than TypeScript.
--force, -fflag—Overwrite the file if it is already there.
--dry-runflag—Print the file instead of writing it.
--yes, -yflag—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
// 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:

  • list draws the collection. It receives rows — the current page, filtered and paginated.
  • row draws one record. It receives row, whose data is the record as the API answered it.
  • item draws one field. It receives formInput, and ItemRender is 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>().options

The 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.