Skip to content

Schema components: layout, display, projection

Compose forms and detail views from layouts, fields, and display projections.

This page documents component taxonomy and projection semantics. Layout components live in packages/core/src/schema/layout/, display components in packages/core/src/schema/display/, and the projection pass in packages/core/src/serialization/descriptor_compiler.ts. Everything below is re-exported from @adonia/core; you reach it through the s builder handed to BaseResource.schema(s).

One tree, three modes

A resource declares one schema. The compiler projects it into create, edit, and detail descriptors — layout is preserved, fields become display entries in detail, and anything that does not belong in a mode is absent from it. There is no visible: false on the wire (protocol §2 rule 3).

export default class PostResource extends BaseResource<typeof Post> {
  static override model = Post
  static override slug = 'posts'

  schema(s: SchemaBuilder): SchemaComponent[] {
    return [
      s.grid().columns(3).components([
        s.section('Content').columns(2).columnSpan(2).components([
          F.text('title').label('Title').required(),
          F.text('body').label('Body').columnSpan('full'),
        ]),
        s.aside().columnSpan(1).components([
          F.text('slug').label('Slug').readonlyOn('edit'),
          s.textEntry('publishedAt').label('Published at').format('datetime'),
        ]),
      ]),
    ]
  }
}

Layout components (§7.1)

Factory Type key Notes
s.section(label?) section Titled group with its own column grid
s.grid() grid Column grid; also the implicit tree root
s.tabs() / s.tab(label?) tabs / tab Tab strip and one panel
s.wizard() / s.step(label?) wizard / step Stepper and one step
s.aside() aside Secondary column of a record page
s.fieldset(label?) fieldset Bordered group of related controls
s.card(label?) card Elevated surface
s.divider(label?) divider Horizontal rule; childless
s.spacer() spacer Empty space / empty grid cell; childless

Every container carries the same DSL, so there is nothing to memorize per component: columns(n), columnSpan(n | 'full'), collapsible(), collapsed(), icon(name), description(text), label(text), visible(cb) (the §7.1 spelling of canSee), and components([...]). divider/spacer have no children, so they carry only the leaf half of that DSL — columnSpan and visible/canSee — plus label and size respectively.

Two rules make layout safe to nest arbitrarily:

  • State keys stay flat. tabs > tab > tabs > tab > section > fieldset > F.text('subtitle') contributes exactly subtitle. Layout never namespaces a key (protocol §2 rule 2), so visibleWhen var paths and reactive.sets[].target always resolve.
  • Emptied containers disappear. When every child of a container is projected away, the container goes with it. The grid root is the sole exception — descriptor.schema is always a single grid (protocol §2 rule 4), possibly with no children.

Display components (§7.1)

Read-only projections of one record attribute, declared directly in the schema tree and rendered in detail only — they carry no form state, so they are absent from create/edit trees, from the validator, and from the fill set.

Factory Type key Typed props
s.textEntry(attr) text-entry format(name), copyable()
s.badgeEntry(attr) badge-entry colors(map), icons(map)
s.imageEntry(attr) image-entry disk(name), visibility(v), height(px)
s.codeEntry(attr) code-entry language(name)
s.keyValueEntry(attr) key-value-entry keyLabel(text), valueLabel(text)
s.relationEntry(attr) relation-entry resource(slug), optionLabel(attr), link(target), multiple()
s.html(attr) html — (markup is sanitized on write, not here)

All seven share label, helper, columnSpan, canSee/visible, and withProps. Each carries its attribute as the node key, so a hand-written s.textEntry('title') is indistinguishable on the wire from the node F.text('title') projects to.

Projection semantics (§7.4)

compile(resource, mode, ctx, record?) resolves four independent gates, in this order, for every node:

  1. canSee(ctx, record) — request-dependent removal.
  2. visibleOn / hiddenOn — static, per rendering context. create/edit compile against 'form'; detail compiles against 'detail'. ('index' is table derivation’s own context, §7.5.)
  3. Category — display entries exist in detail only.
  4. readonlyOn(mode) — the node stays, and gains readonly: true on its props for the matching form sub-mode.

Gates 1–3 are omission, and omission is total: the node is missing from the tree, from descriptor.stateKeys, from the compiled validator, and from the fill set, all in one pass. That is what makes mass assignment impossible by construction (§19) — the fill set is derived from the compiled schema for the exact mode of the submission, never from the payload. readonlyOn is the one gate that keeps the node: a read-only field still renders and still validates, but it is dropped from that sub-mode’s fill set, so a tampered value cannot reach the model.

In detail, each field is replaced by its displayType (text-input → text-entry, image → image-entry, rich-text → html, …). That is an instance getter, not just a class static: a choice field (select, radio, checkbox-list) projects to badge-entry when its own options carry colours and to text-entry when they do not, and the same split drives its columnType (badge vs text, §7.5).

Only chrome props survive the projection — label, helper, columnSpan, icon, description, prefix, suffix, plus format, which protocol §8 mandates for the date family (datetime → text-entry with format: 'datetime'). Validation props (required, maxLength, nullable, …) and input-only props (placeholder, default, disabled, readonly) are dropped, and the reactive block is dropped entirely: visibility was already resolved server-side.

collectFields(resource, ctx?, record?, context?) is the shared walk behind all of this. Passing a context applies gate 2; validator and fill derivation pass 'form' (the default of collectFieldMetas), while table derivation passes nothing because it projects into 'index' itself.

Tests

packages/core/tests/layout_components.spec.ts (component × mode descriptor snapshots under tests/fixtures/snapshots/, shared DSL, flat state keys, empty-layout omission) and packages/core/tests/projection.spec.ts (the four gates against tree + stateKeys + validator + fill set).

Navigation

Type to search…

↑↓ navigate↵ selectEsc close