react-resource-view

Sub-views & tabs

Nesting a resource inside another one's detail page.

The shape of the problem#

An author's page is not only the author. It is the author, their articles, their comments, their activity — each of which is a collection in its own right, and each of which should be filtered by the record on screen without that filter being the reader's to remove.

A sub-view is a full view context nested inside another: it fetches, it filters, it paginates, it writes. It is not a read-only panel.

Declaring sub-views#

subViewResource.list is the tabs of a detail page. An entry takes one of two shapes — a nested resource, or a component of your own.

views: {
  [ActionList.read]: {
    subViewResource: {
      list: [
        {
          // A nested resource, filtered by the record on screen.
          resource: articles,
          resourceAction: ActionList.list,
          name: "Articles",
          icon: FileText,
          onInitViewResource: (view, parent) => ({
            ...view,
            filter: { author: parent?.data?.["@id"] },
          }),
        },
        {
          // Or a tab of your own, with no resource behind it.
          slug: "activity",
          name: "Activity",
          icon: Activity,
          viewComponent: AuthorActivity,
        },
      ],
    },
  },
}
NameTypeDefaultDescription
resource / resourceIdViewResourceInterface | stringThe nested resource. Omit for a free-form tab.
resourceActionActionListWhich of its views the tab renders.
slugstringThe tab's identifier in the URL. Defaults to the resource's @id.
namestringThe tab label. Defaults to the resource's name.
iconIconTypeThe tab icon. Defaults to the resource's.
viewComponentFCCustom rendering, taking precedence over the resource's own — the free-form tab.
onInitViewResource(view, parent) => viewWhere the nested context is derived from the surrounding one.
filterFilterInterfaceA static filter, when nothing has to be derived.

MultiViewTab renders the tab bar. It is exported, so a detail page laid out by hand can place it wherever it belongs.

Deriving from the parent#

onInitViewResource receives the context being built and the surrounding one, and returns the context to use. It runs once, when the sub-view mounts.

onInitViewResource: (view, parent) => ({
  ...view,
  // Filters the list…
  filter: { author: parent?.data?.["@id"] },
  // …and pre-fills the create form opened from inside it.
  defaultData: { author: parent?.data?.["@id"] },
})

Filter and defaultData together

Filtering alone gives a list of the author's articles whose create button makes an article belonging to nobody. Setting defaultData as well is what makes “new article” mean “new article by this author”.

Filters injected this way are marked as generated, so “clear search” keeps them — as the filters page describes. Losing that one would show every author's articles inside a page about one of them.

Nesting in the URL#

The open tab is a segment of the context, and a child view appends its own three segments after it. So a nested state is addressable, and the back button walks out of it a step at a time.

/admin/authors/read/7/articles
// subResource: which tab is open

/admin/authors/read/7/articles/categories/create
// the child view's own three segments
  • Switching tabs does not reset the scroll position — the views ask the router not to.
  • A sub-view's filter is not written to the URL: only the outermost context owns the query string, which is what keeps two nested lists from overwriting each other.

A child view in a dialog#

Sometimes the nested thing should not replace the page. A category has to be created in the middle of writing an article, and the article must still be there afterwards.

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

// A second context, rendered over the first — creating a category without
// leaving the article being written.
<ChildViewResourceDialog
  resource={categories}
  resourceAction={ActionList.create}
/>

ChildViewResourceDialog renders a second context over the first. The parent is untouched, and the resource's onChange is what tells the field behind the dialog to reload its options.

behavior.openIn

A view can also ask to open in a popup rather than in place — with behavior: { openIn: "popup" } — together with closeAfterUpdate and refreshDataAfterUpdate, which decide what happens once the write lands.