Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions content/docs/ui/create-vs-edit-form.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,25 @@ The create-form subset is not an arbitrary pick; it is *derivable* from signals
| `readonly` / formula / rollup / autonumber / system-stamped | **never** on create (you can't set it) |
| `defaultValue` present | **can be omitted** from create (it self-fills) |
| `hidden` | off everywhere by default |
| `group` | which section the field belongs to (semantic, travels with the model) |
| `group` | which section the field belongs to (semantic, travels with the model — and only once that group is **declared** in `fieldGroups`; see §2) |
| *declaration order* | the order you write fields in **is** the default order everywhere — there is no `field.order` |

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.

### 2. The default (edit) form derives from `field.group`
### 2. The default (edit) form derives from `field.group` — as authorized by `fieldGroups`

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.
`field.group` on its own does not create a section. The **authorization source is the object's `fieldGroups` declaration**: `deriveFieldGroupLayout` (ADR-0085 §5) — the single derivation every renderer and the i18n walker consume — sections only those fields whose `group` matches a declared `fieldGroups[].key`.

- An **undeclared** `group` renders **exactly like no `group` at all**: the field drops into a trailing untitled bucket.
- Declare **no** `fieldGroups` at all and grouping does not apply — the derivation yields nothing and you get a flat form.
- Either way, `os lint` (and `os build` / `os validate`) reports every unmatched reference as **`field-group-undeclared`**.

So tag the fields **and** declare the groups — [`contact.object.ts`](#runnable-example) does both. With both halves present, the full edit form materialises each declared group into a section, and you can omit the `form` view entirely and let the platform derive the grouped form. Two ways the derived form still differs from the hand-written one below:

- fields the platform injects and you never grouped (`owner_id`, under `sharingModel: 'private'`) surface in that trailing untitled section;
- `columns` is a **form-view** knob, and the group declaration has no column count to give — a derived section carries only `key` / `label` / `icon` / `description` / `collapse` and its member fields — so a hand-written `columns: 2` is not part of what derivation gives back.

When you do write the form, 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.

### 3. Hand-shape the create form *only when layout or flow diverges*

Expand All @@ -50,7 +61,8 @@ export const ContactViews = defineView({
addRecord: { enabled: true, mode: 'form', formView: 'create' },
},

// Full edit form — grouped by field.group; bare strings inherit field defs.
// Full edit form — one section per group DECLARED in the object's
// `fieldGroups`; bare strings inherit field defs.
form: {
type: 'simple', data,
sections: [
Expand Down Expand Up @@ -102,7 +114,7 @@ Rule of thumb: **"different field subset" → derive. "different layout or flow"

## Runnable example

- Object: [`examples/app-showcase/src/data/objects/contact.object.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/data/objects/contact.object.ts) — flat, grouped, intent-tagged field set.
- Object: [`examples/app-showcase/src/data/objects/contact.object.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/data/objects/contact.object.ts) — flat, intent-tagged field set, with the four `fieldGroups` its `group` tags point at.
- Views: [`examples/app-showcase/src/ui/views/contact.view.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/ui/views/contact.view.ts) — full edit form + sparse `formViews.create` + `addRecord` binding.

## Anti-patterns
Expand Down
38 changes: 28 additions & 10 deletions content/docs/ui/field-grouping-and-order.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,34 @@ So "flat vs grouped" is not a contradiction — it's one flat set seen through d

There are **two** grouping concepts. Keep them distinct:

**1. Semantic grouping — `field.group` (on the object).** A field's logical home ("billing", "contact_info", "system"). It travels with the model. The Studio field editor folds fields by it, and auto-generated forms use it as the **default** sectioning — so you are not starting from zero.
**1. Semantic grouping — `field.group` + `fieldGroups` (on the object).** A field's logical home ("billing", "contact_info", "system"). It travels with the model. The Studio field editor folds fields by it, and auto-generated forms use it as the **default** sectioning — so you are not starting from zero.

Semantic grouping has **two halves, and you need both**. `fieldGroups` on the object is the **authorization source** for the derivation: `deriveFieldGroupLayout` (ADR-0085 §5) — the one implementation every renderer and the i18n walker consume — sections only those fields whose `group` matches a declared `fieldGroups[].key`. An **undeclared** `group` renders **exactly like no `group` at all** (the field falls into a trailing untitled bucket); with **no** `fieldGroups` declared at all, grouping does not apply and the form comes out flat. `os lint` (and `os build` / `os validate`) reports the mismatch as **`field-group-undeclared`**.

{/* os:check */}
```ts
fields: {
name: Field.text({ label: 'Full name', group: 'contact' }),
email: Field.email({ label: 'Email', group: 'contact' }),
stage: Field.select({ label: 'Stage', group: 'status', options: [/* … */] }),
}
import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Contact = ObjectSchema.create({
name: 'showcase_contact',
label: 'Contact',

// The DECLARATION half — array order is display order (there is no `order` key).
fieldGroups: [
{ key: 'contact', label: 'Contact' },
{ key: 'status', label: 'Status' },
],

// The MEMBERSHIP half — each `group` must match a `key` declared above.
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: 'New', value: 'new' }] }),
},
});
```

**2. Layout grouping — form `sections` (on a view).** A specific form's explicit arrangement: which fields, which section, how many columns, collapsible. It can **inherit** `field.group` as the default or **override** it per form.
**2. Layout grouping — form `sections` (on a view).** A specific form's explicit arrangement: which fields, which section, how many columns, collapsible. It can **inherit** the object's declared groups as its default or **override** them per form.

```ts
form: {
Expand Down Expand Up @@ -68,17 +85,18 @@ Keeping grouping off the field (beyond an optional semantic hint) is what lets *

## Runnable example

- [`examples/app-showcase/src/data/objects/contact.object.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/data/objects/contact.object.ts) — fields tagged with `group`.
- [`examples/app-showcase/src/data/objects/contact.object.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/data/objects/contact.object.ts) — four declared `fieldGroups`, and the fields tagged with the matching `group`.
- [`examples/app-showcase/src/ui/views/contact.view.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/ui/views/contact.view.ts) — sections that materialise those groups.

## Anti-patterns

- **Adding structural nesting to the data model to satisfy a form.** The model is flat; let the form group.
- **Re-typing the grouping in every form.** Declare `field.group` once; forms inherit and only override on real divergence.
- **Re-typing the grouping in every form.** Declare the groups (`fieldGroups`) and tag the fields (`field.group`) once; forms inherit and only override on real divergence.
- **Tagging fields with a `group` you never declared in `fieldGroups`.** The tag reads as intent but authorizes nothing — the fields render ungrouped, and `os lint` says so as `field-group-undeclared`.
- **Assuming a grid "group" will section your form fields.** It buckets rows by value — a different axis entirely.

## See also

- [Create form ≠ edit form](/docs/ui/create-vs-edit-form).
- Reference: [Field schema](/docs/references/data/field) (`group`), [View schema](/docs/references/ui/view) (`sections`, `grouping`).
- Reference: [Object schema](/docs/references/data/object) (`fieldGroups`), [Field schema](/docs/references/data/field) (`group`), [View schema](/docs/references/ui/view) (`sections`, `grouping`).
- Studio: [Object Designer](/docs/references/studio/object-designer) — field editor groups by `field.group`.
Loading