|
| 1 | +--- |
| 2 | +title: Create form ≠ edit form |
| 3 | +description: The new-record form asks 5 fields; the full edit form shows 40 grouped into sections. Derive both from one flat field set; only hand-shape the create form when layout or flow genuinely diverges. |
| 4 | +--- |
| 5 | + |
| 6 | +# Create form ≠ edit form |
| 7 | + |
| 8 | +## Scenario |
| 9 | + |
| 10 | +> The form for **creating** a record should ask only a handful of fields (the essentials). The form for **editing** an existing record shows the full record, grouped into sections. How do I model this without maintaining two field lists that drift apart? |
| 11 | +
|
| 12 | +## Recommended solution |
| 13 | + |
| 14 | +**Do not author two forms. Author one field set with enough intent that both forms derive from it — then override only when you must.** |
| 15 | + |
| 16 | +### 1. Put the intent on the fields |
| 17 | + |
| 18 | +The create-form subset is not an arbitrary pick; it is *derivable* from signals that already live on each field: |
| 19 | + |
| 20 | +| Field signal | Effect on the create form | |
| 21 | +|---|---| |
| 22 | +| `required: true` | **must** appear on create | |
| 23 | +| `readonly` / formula / rollup / autonumber / system-stamped | **never** on create (you can't set it) | |
| 24 | +| `defaultValue` present | **can be omitted** from create (it self-fills) | |
| 25 | +| `hidden` | off everywhere by default | |
| 26 | +| `group` | which section the field belongs to (semantic, travels with the model) | |
| 27 | +| *declaration order* | the order you write fields in **is** the default order everywhere — there is no `field.order` | |
| 28 | + |
| 29 | +So a sensible create form is: *editable, required-or-core fields, in declaration order* — and it **falls out** of the object. This is ADR-0047's guardrail: **omission is correct** — emit nothing extra and you still get a complete, correct form. |
| 30 | + |
| 31 | +### 2. The default (edit) form derives from `field.group` |
| 32 | + |
| 33 | +The full edit form materialises each `field.group` into a section. You can omit it entirely and let the platform derive an equivalent grouped form. When you do write it, list fields as **bare strings** so each one inherits its type / validation / FLS / default from the object — the form carries layout only, never data semantics. |
| 34 | + |
| 35 | +### 3. Hand-shape the create form *only when layout or flow diverges* |
| 36 | + |
| 37 | +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 — not a from-scratch restatement: |
| 38 | + |
| 39 | +```ts |
| 40 | +import { defineView } from '@objectstack/spec'; |
| 41 | + |
| 42 | +const data = { provider: 'object' as const, object: 'showcase_contact' }; |
| 43 | + |
| 44 | +export const ContactViews = defineView({ |
| 45 | + list: { |
| 46 | + type: 'grid', data, |
| 47 | + columns: [{ field: 'name' }, { field: 'email' }, { field: 'company' }, { field: 'stage' }], |
| 48 | + // Bind the "+ Add record" entry point to the slim create form: |
| 49 | + addRecord: { enabled: true, mode: 'form', formView: 'create' }, |
| 50 | + }, |
| 51 | + |
| 52 | + // Full edit form — grouped by field.group; bare strings inherit field defs. |
| 53 | + form: { |
| 54 | + type: 'simple', data, |
| 55 | + sections: [ |
| 56 | + { name: 'contact', label: 'Contact', columns: 2, fields: ['name', 'email', 'phone'] }, |
| 57 | + { name: 'work', label: 'Work', columns: 2, fields: ['company', 'title'] }, |
| 58 | + { name: 'status', label: 'Status', columns: 2, fields: ['stage', 'lead_score'] }, |
| 59 | + { name: 'notes', label: 'Notes', columns: 1, fields: ['notes'] }, |
| 60 | + ], |
| 61 | + }, |
| 62 | + |
| 63 | + formViews: { |
| 64 | + // Sparse create override: only the core fields, one section. Omits |
| 65 | + // lead_score (readonly), stage (defaulted), notes (edit-time only). |
| 66 | + create: { |
| 67 | + type: 'simple', data, title: 'New contact', |
| 68 | + sections: [ |
| 69 | + { label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] }, |
| 70 | + ], |
| 71 | + }, |
| 72 | + }, |
| 73 | +}); |
| 74 | +``` |
| 75 | + |
| 76 | +The binding is `addRecord.mode: 'form'` + `addRecord.formView: 'create'` ([`AddRecordConfigSchema`](/docs/references/ui/view)). No `formViews.create` → the create entry derives a default. Present → it wins for create. |
| 77 | + |
| 78 | +## Why |
| 79 | + |
| 80 | +Three layers, each *derive + only store differences* — never "re-list all 40 fields": |
| 81 | + |
| 82 | +``` |
| 83 | +1. Derived default derive(object, 'create' | 'edit') ← free, no authoring |
| 84 | +2. Author override formViews.create (sparse patch) ← this recipe; only on real divergence |
| 85 | +3. Tenant override org overlay delta (ADR-0005) ← a single org wants its own form |
| 86 | +``` |
| 87 | + |
| 88 | +Welding two independent full forms is the **Salesforce page-layout tax**: add a required field, forget the create form → runtime "missing required field" on create; rename a field → silent drift. Keeping data semantics on the object (never on the form) means a form can only ever drift on *which fields appear* — a flat name list that **reference-integrity diagnostics catch as a hard failure** in the AI loop (ADR-0047 §3.5, ADR-0033). That guardrail is what makes the escape hatch safe to hand to an AI author. |
| 89 | + |
| 90 | +The create fields are a **prefix/subset of the edit fields in the same order and groups**, so "quick-create 4 fields → save → land on the full record" stays visually continuous. Derivation preserves that for free; two hand-authored forms break it. |
| 91 | + |
| 92 | +## When to actually reach for the override |
| 93 | + |
| 94 | +| Your real need | Use | |
| 95 | +|---|---| |
| 96 | +| Create asks fewer fields (required + a few core) | **Pure derivation** — don't hand-write | |
| 97 | +| Create groups differently but is just a smaller subset | Usually still derivation (a 5-field form needs no sections) | |
| 98 | +| Create is a **wizard / multi-step**, has create-only copy, conditional reveal, bespoke layout | **Hand-write `formViews.create`** | |
| 99 | + |
| 100 | +Rule of thumb: **"different field subset" → derive. "different layout or flow" → override.** Writing a full create form just to drop a few fields walks straight back into the two-artifact drift trap. |
| 101 | + |
| 102 | +## Runnable example |
| 103 | + |
| 104 | +- Object: [`examples/app-showcase/src/objects/contact.object.ts`](https://github.com/objectstack-ai/framework/blob/main/examples/app-showcase/src/objects/contact.object.ts) — flat, grouped, intent-tagged field set. |
| 105 | +- Views: [`examples/app-showcase/src/views/contact.view.ts`](https://github.com/objectstack-ai/framework/blob/main/examples/app-showcase/src/views/contact.view.ts) — full edit form + sparse `formViews.create` + `addRecord` binding. |
| 106 | + |
| 107 | +## Anti-patterns |
| 108 | + |
| 109 | +- **Two full hand-authored field lists** (`contact_create_form` + `contact_edit_form`). Guaranteed drift; the create form silently misses new required fields. |
| 110 | +- **Restating field type / validation / options on the form.** Data semantics live on the object only; the form chooses *which fields and where*. |
| 111 | +- **Reaching for `formViews.create` to drop fields.** That's derivation's job — reserve the override for genuine layout/flow divergence. |
| 112 | + |
| 113 | +## See also |
| 114 | + |
| 115 | +- Decision record: **ADR-0047** (object UI run modes — derive-default + override). |
| 116 | +- [Field grouping & order](/docs/guides/solutions/field-grouping-and-order) — the three different meanings of "group". |
| 117 | +- Reference: [View / FormView schema](/docs/references/ui/view), [Field schema](/docs/references/data/field). |
0 commit comments