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/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 id 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.
--jsxflagWrite JavaScript rather than TypeScript.
--force, -fflagOverwrite the file if it is already there.
--dry-runflagPrint the file instead of writing it.
--yes, -yflagNever 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
// 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:

  • listComponent draws the collection. It receives rows — the current page, filtered and paginated.
  • rowComponent draws one record. It receives row, whose data is the record as the API answered it.
  • itemComponent 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: 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 ?? false

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