CRUD / foldkit-crud

Showing values

How a column shows

A Display is foldkit-form's Input from the reading side: how one selected member shows, described without a renderer.

ts
const Post = Blog.Post.pipe(
  Entity.annotateMembers({
    id: Display.of(Display.hidden()), // read, so a row can be opened by it, and not shown
    cents: Display.of(Display.number(cents => `$${(cents / 100).toFixed(2)}`)),
  }),
)

Display.show(column.display, row[column.key], { yes: 'Live', no: 'Draft' }) // the cell's text
KindFor
Text, Numbera value as it is, or through format
Flaga boolean, in the words given (yes / no)
Hiddena member read and not shown
Refa relation selected with true: its id, or ids
Nesteda relation read through a Selection: the target's own columns, with shape one, many, or page

A display is resolved from the most to the least explicit source: Display.of metadata on the member, how the relation was selected, then the shape of the schema. Display.show is the text any view can fall back on; a view that wants a link or a badge reads display and the value. foldkit-mixins-crud draws a list as a table and a detail as a description list from exactly this.

There is one primitive, as with a form's controls: a kind, whether it is shown, its data, and its text. The kinds above are made with Display.kind, and so is yours:

ts
const Badge = Display.kind<{ readonly tone: string }>('Badge', {
  text: (_, value) => String(value), // the floor: what it says when nothing draws it specially
})

const Post = Blog.Post.pipe(Entity.annotateMembers({ status: Display.of(Badge.of({ tone: 'soft' })) }))

Badge.is(display) narrows display.data. A drawn view draws it specially once given a renderer for Badge.

A relation picker's choices

A form names a relation's target Entity and leaves listing it to you, because a relationship existing is no licence to read a table. A list is that licence: a query you declared and your server authorizes. A list with a choice offers its loaded rows as choices, and Crud.options hands each picker of a form the list over its target:

ts
const pickers = Crud.options(EditPostForm, [AuthorList])

Placed.view(model, h, { options: pickers(model) }) // with foldkit-mixins-form

pickers(model) is keyed by the form's keys: { authorId: [{ value, label }, …] }. A list is matched to a picker by Entity, so one author list serves authorId and editorId alike. A picker whose target no list is over, or whose list has no choice, throws when Crud.options is called, not when the form is drawn. AuthorList.choices(model) is one list's choices on its own. Both are empty until the page is loaded, and hold only the rows loaded so far.

When a picker searches

ts
const AuthorList = Authors.at({
  data: Data,
  // What the picker's search box holds is the form's; here it is the query's input.
  input: model => Option.some({ search: EditPostForm.search(model.editor.form, 'authorId') }),
})

const pickers = Crud.options(EditPostForm, [AuthorList], {
  chosen: model => model.editor.form, // the form's Model, wherever the page keeps it
})

const subscriptions = Data.subscriptions({ authors: AuthorList.active, chosen: pickers.active })

A search narrows the list, and what the picker already holds may no longer be in it. With chosen, those rows stay among the choices, first, and pickers.active makes them a requirement so Remote reads them and they can be named. Without it a select would show a post's author as blank the moment a search stopped finding them.