react-resource-view

Choosing a layout

Seven layouts over one resource, and how the reader switches.

A collection is not one screen. The same articles are a table when you are comparing them, a board when you are moving them along, and a calendar when you are scheduling them. All three are the same resource with a different components.list.

The seven layouts#

Table

layout.table()
One column per field of the resource's form, editable in place. The default for anything with more than three fields worth comparing.

Cards

layout.cards()
A grid of cards, columns per row. Right when a record has a title and a couple of attributes.

Item list

layout.items()
A plain vertical list. The lightest of the seven, and the one that survives a narrow column.

Board

layout.board()
A board grouped by a field — a kanban. Needs groupBy and columns.

Split

layout.split()
The list on the left, the selected item on the right. Replaces the separate detail page.

Calendar

layout.calendar()
By day, week or month, from a date field. dateField says which.

Timeline

layout.timeline()
A Gantt-like band per row, grouped by a field — rooms, staff, vehicles.

Declaring several#

views.list.layouts is a list. Declare more than one and the view renders a switcher; the reader's choice is written into the URL, so it survives a reload and travels in a shared link.

import { layout } from "react-resource-view"

form: articleForm,
views: {
  list: {
    layouts: [
      layout.table(), // the default: the first one listed
      layout.cards({ columns: 3 }),
      layout.split({ readInList: true }),
    ],
  },
}

Five layouts over the same articles

The first one wins

With no layout in the URL, the first of the list is used. Ordering them is how you choose the default, and a layout id that no longer exists falls back to it rather than rendering nothing.

The switcher is not a loose control floating above the rows: every list draws one header line, and the switcher sits in it. From left to right — the resource's icon in a badge, the view's title — derived from the resource's name — and description, the layout switcher, then the export and create buttons, with a rule under the lot.

createViewResource("articles", {
  name: "Articles",         // the heading: "Articles"
  icon: Newspaper,          // the badge, and the same mark the menu uses
  description: "Everything published, and everything on its way there.",
  views: {
    list: { layouts: [layout.table(), layout.cards()] },
  },
})

Nothing is declared to turn it on. It is assembled from what the resource already says, and each piece disappears when there is nothing to draw: no icon, no badge; no description, one line instead of two; one layout, no switcher; permissions.create refused, no button.

A nested list keeps quiet

A list rendered inside another view — a tab, a resource embedded in a page of your own — leaves the naming out: whatever contains it has already written its title, and saying it twice is only saying it twice. Its switcher and its buttons stay.

The icon shown is the resource's, not the layout's. A layout carries an icon of its own — it is what the switcher draws — and a header taking that one would change the heading of the collection every time the reader changed the layout.

The filter bar is not part of it: it is a row of its own, below the rule, and it has its own page.

How a layout is identified#

A layout's id is its type, so renaming its label breaks no shared link. Pass an id only when the same layout appears twice.

// A layout is identified by its type — in the URL, ?variant=table.
layout.table() // id: "table"
layout.cards() // id: "cards"

// The label changes what the switcher shows, not the id: still "board".
layout.board({ label: "Kanban", groupBy: "status", columns: STATUSES })

// The same type twice needs an id of its own.
layout.table({ id: "compact", label: "Compact", fields: ["title"] })

What every layout shares#

Besides its own options — columns, groupBy, dateField… — every layout accepts the same few, and only for that layout:

  • fields — the columns of the table, the lines of a card, in order: fields: ["title", "status"]. Checked against the item type.
  • form — adjusts the resource's form for this layout only, merged over it.
  • fullWidth — this layout takes the page's whole width.
  • label, icon — what the switcher shows; id — what the URL says.
  • components.list, components.row, components.item.

Those three rendering slots are what a layout actually sets: components.list draws the collection, components.row one record, and components.item one field of a record. What belongs to the list rather than to one layout — pageSize, filters, sort, bulkActions, components.top, components.empty — is declared on views.list, and holds whichever layout the reader picks.

Writing your own#

A built-in layout is a preset over the same shape as layout.custom(), nothing more. A layout of your own is three components and one call:

import { layout } from "react-resource-view"

// A built-in layout is a preset. Nothing stops you writing your own.
export const heatmapLayout = (options = {}) =>
  layout.custom({
    type: "heatmap",
    icon: Flame,
    components: {
      list: MyHeatmap, // receives { rows }
      row: MyHeatCell, // receives { row }
      item: MyHeatValue, // receives { formInput }
    },
    ...options,
  })

Inside those components, useList gives the rows and the mutations, and useCurrentViewResourceContext gives the whole context — loading state, filters, selection — and useLayout().options the layout's own options.

You need not type any of it: the package ships a command that writes that file where you want it, components and layout.custom() call included.

npx react-resource-view create-view-variant Heatmap --dir src/views

Create your own layout takes the generated file apart, line by line.

The next three pages take the built-in layouts one family at a time: table and cards, split and board, and calendar and timeline — then one you write yourself.