ConstruireInterface
Cette page n'est pas encore traduite. Elle s'affiche en anglais.

Forms

Derive create and edit forms from one flat field set, group fields into sections without drift, and control what happens after submit.

A form is a projection of your object's fields — not a second field list. You declare fields once on the object; forms choose which fields and where. Data semantics (types, validation, defaults, field-level security) never live on the form, so forms can't drift into lying about the data.

Form view types (simple, tabbed, wizard, modal, …) and the public-form REST contract are covered in Views. This page is about the layout questions: create vs edit, grouping, ordering.

Create form ≠ edit form

The new-record form should ask a handful of essentials; the edit form shows the full record in sections. Don't author two forms. The create-form subset is derivable from intent that already lives on each field:

Field signalEffect on the create form
required: trueMust appear on create
readonly / formula / rollup / autonumber / system-stampedNever on create — you can't set it
defaultValue presentCan be omitted — it self-fills
hiddenOff everywhere by default
groupWhich section the field belongs to
Declaration orderIs the default display order — there is no field.order

So the sensible create form — editable, required-or-core fields, in declaration order — falls out of the object with zero authoring. Omission is correct: emit nothing extra and you still get a complete, correct form. The full edit form derives too, materializing each field.group into a section.

Hand-shape the create form only when layout or flow diverges

The escape hatch is a named form view bound to the create entry point. Author it as a sparse override — mostly a list of field names:

import { defineView } from '@objectstack/spec'

const data = { provider: 'object' as const, object: 'showcase_contact' }

export const ContactViews = defineView({
  list: {
    type: 'grid', data,
    columns: [{ field: 'name' }, { field: 'email' }, { field: 'company' }, { field: 'stage' }],
    // Bind the "+ Add record" entry point to the slim create form:
    addRecord: { enabled: true, mode: 'form', formView: 'create' },
  },

  // Full edit form — grouped by field.group; bare strings inherit field defs.
  form: {
    type: 'simple', data,
    sections: [
      { name: 'contact', label: 'Contact', columns: 2, fields: ['name', 'email', 'phone'] },
      { name: 'work',    label: 'Work',    columns: 2, fields: ['company', 'title'] },
      { name: 'status',  label: 'Status',  columns: 2, fields: ['stage', 'lead_score'] },
    ],
  },

  formViews: {
    // Sparse create override: only the core fields, one section.
    create: {
      type: 'simple', data, title: 'New contact',
      sections: [
        { label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] },
      ],
    },
  },
})

The binding is addRecord.mode: 'form' + addRecord.formView: 'create'. No formViews.create → the create entry derives a default. Present → it wins for create.

Your real needUse
Create asks fewer fields (required + a few core)Pure derivation — don't hand-write
Create groups differently but is just a smaller subsetUsually still derivation
Create is a wizard / multi-step, has create-only copy, conditional revealHand-write formViews.create

Rule of thumb: different field subset → derive. Different layout or flow → override. Writing a full create form just to drop fields walks straight into the two-artifact drift trap: add a required field, forget the create form, get a runtime "missing required field" on create.

Because the create fields are a subset of the edit fields in the same order and groups, "quick-create 4 fields → save → land on the full record" stays visually continuous. Derivation preserves that for free.

Field grouping and order

The model is a flat field set. A grid shows flat columns. A form needs sections. That's not a contradiction — it's one flat set seen through different lenses:

LensField shapeWhy
Object definitionFlatThe model describes what data exists
Table / gridFlat columnsA grid is a record × field matrix
Form / record pageGrouped sectionsA human reading one record needs chunking

There are two grouping concepts. Keep them distinct:

1. Semantic grouping — field.group (on the object). A field's logical home. It travels with the model and seeds the default sectioning of auto-generated forms:

// Fields of an object; the object around them is omitted.
fields: {
  name:  Field.text({ label: 'Full name', group: 'contact' }),
  email: Field.email({ label: 'Email',    group: 'contact' }),
  stage: Field.select({
    label: 'Stage',
    group: 'status',
    options: [
      { label: 'Lead', value: 'lead' },
      { label: 'Customer', value: 'customer' },
    ],
  }),
}

2. Layout grouping — form sections (on a view). A specific form's explicit arrangement: which fields, which section, how many columns, collapsible. It inherits field.group as the default and overrides it per form when needed.

// Keys of a view container; `data` and its other keys are omitted.
form: {
  type: 'simple',
  sections: [
    { name: 'contact', label: 'Contact', columns: 2, fields: ['name', 'email', 'phone'] },
    { name: 'status',  label: 'Status',  columns: 2, fields: ['stage'] },
  ],
}

Order is the order you author in. There is no field.order — field declaration order on the object is the default display order everywhere, and sections (and the fields inside them) are ordered lists.

The "group" trap. A grid view's groupByField groups records (rows) by a field's value — all stage = qualified rows together. A form section groups fields (columns) visually. Same word, two unrelated axes. A grid group will never section your form.

What happens after submit

Add submitBehavior to a form view to control the post-submit experience:

// Keys of a form view; its other keys are omitted.
submitBehavior: {
  kind: 'thank-you',
  title: 'Thanks!',
  message: 'A specialist will reach out within 24 hours.',
}
kindBehavior
thank-you (default)Replace the form with a confirmation panel (title, message)
redirectNavigate to url after delayMs (default 0) — great for marketing pages
continueReset the form for another response — kiosks, batch entry
next-recordAdvance to the next record in a queue (internal mode)

Both public and internal forms also honor ?prefill_<field>=<value> URL params — seed forms from email links or campaign pages. Prefill is a UX shortcut, not a permissions bypass: values still go through validation and (for public forms) the server-side field whitelist.

/console/f/contact-us?prefill_company=Acme&prefill_email=ada@example.com
/console/forms/quick_create?prefill_lead_source=event_booth_2026

Record detail is driven by roles, not a form binding

There is no object-level key that pins a form view to the record-detail screen. Detail rendering derives from the object's cross-surface semantic roles: nameField (display name), highlightFields (the strip of most important fields), stageField (the lifecycle progress stepper), and fieldGroups + Field.group (sectioning shared by forms, modals, and detail pages). When a record page needs a bespoke layout beyond what the roles derive, assign the object a custom Page.

Anti-patterns

  • Two full hand-authored field lists (contact_create_form + contact_edit_form). Guaranteed drift.
  • Restating field type / validation / options on the form. Data semantics live on the object only.
  • Re-typing the grouping in every form. Declare field.group once; override only on real divergence.
  • Adding structural nesting to the data model to satisfy a form. The model stays flat; the form groups.

Where to go next

PageWhy
ViewsForm view types, public form sharing, the full view schema
Data modelWhere fields — and their intent — are declared
Actionstype: 'form' actions that launch a form view
PagesCustom record pages beyond derived layouts
PermissionsField-level security forms respect