Skip to content

Layout ​

The layout tree starts at a FormRoot. Containers nest: the root, groups and repeating groups hold a vertical flow of children; rows hold cells; grids hold placements. Every layout property is part of the model, so the editor's preview and a production FormView render the same thing.

Constants ​

These bound every layout.

ConstantValueUsed for
gapScale[0, 4, 8, 12, 16, 24]The only allowed rowGap and columnGap values.
defaultGap16Default for every gap.
rowColumnOptions{6, 12}Allowed FormRow.columns and FreeGrid.columns.
maxRowCells6Most cells in one row.
defaultStackBelow600.0Width in px below which a row stacks.
gridRowHeight88.0Fixed height of one grid row. Not stored in the model.

Containers ​

NodePropertiesAt fill time
FormRootid, children, rowGapA vertical flow under the form's title and description.
FormGroupid, title, collapsible, initiallyCollapsed (only when collapsible), rowGap, childrenA titled section that collapses when allowed.
RepeatingGroupid, title, minItems (default 0), maxItems (null for no limit), collapsible, initiallyCollapsed, rowGap, childrenOne block per entry, each with its own values. Add stops at maxItems; Remove is disabled at minItems.
FormRowid, columns (6 or 12), cells (1 to 6 FormCell(child, span)), columnGap, stackBelowCells side by side by span.
FreeGridid, columns (6 or 12), rows (at least 1), placements (GridPlacement(child, x, y, width, height)), rowGap, columnGapItems at fixed cells; each row is 88 px tall.

Groups ​

dart
FormGroup(
  id: 'deviation',
  title: 'Deviation',
  collapsible: true,
  rowGap: 12,
  children: [
    const FieldSlot(id: 'slot_batch', fieldKey: 'batch'),
  ],
)

Repeating groups ​

A repeating group stores its value under its own id as a list of entry maps, one map per entry. Each entry is a new value scope: a formula inside the group reads the fields of the same entry. A repeating group cannot contain another repeating group.

dart
RepeatingGroup(
  id: 'materials',
  title: 'Affected materials',
  minItems: 1,
  maxItems: 10,
  children: [/* rows and slots for material, qty, unit_cost, line_cost */],
)

Rows ​

A row divides its width into 6 or 12 columns. Each cell takes a span of those columns. Spans must fit in the row, and no span may be smaller than its child's minimum (see below).

dart
FormRow(
  id: 'deviation_row',
  columns: 12,
  columnGap: 16,
  stackBelow: 720,
  cells: const [
    FormCell(child: FieldSlot(id: 'slot_title', fieldKey: 'title'), span: 6),
    FormCell(child: FieldSlot(id: 'slot_severity', fieldKey: 'severity'), span: 3),
    FormCell(child: FieldSlot(id: 'slot_discovered', fieldKey: 'discovered_at'), span: 3),
  ],
)

A row cell holds a field slot, content, or a group (a group in a cell acts as a column). Rows, grids and repeating groups are flow-only: they cannot appear anywhere inside a row cell or a grid, not even inside a group there.

Free grids ​

A free grid places each child at x, y with a width and height in grid cells. Placements must stay inside columns × rows, must not overlap, and must be at least the child's minimum size. Row height is fixed at gridRowHeight (88 px), so a height of 2 is two rows plus the gap between them.

dart
FreeGrid(
  id: 'assessment',
  columns: 12,
  rows: 2,
  placements: const [
    GridPlacement(child: FieldSlot(id: 'slot_what', fieldKey: 'what_happened'), x: 0, y: 0, width: 8, height: 2),
    GridPlacement(child: FieldSlot(id: 'slot_capa', fieldKey: 'capa_owner'), x: 8, y: 0, width: 4, height: 1),
    GridPlacement(child: FieldSlot(id: 'slot_confidence', fieldKey: 'confidence'), x: 8, y: 1, width: 4, height: 1),
  ],
)

Minimum sizes ​

minimumSizeOf(node, fields) returns a FormElementSize(columns, rows, minWidth), with columns counted out of 12. The model refuses spans and placements below it, the editor clamps resizes to it, and FormView uses minWidth to decide when to stack.

ElementColumns × rowsMin width
toggle, number, formula, rating, date-only; heading, divider, spacer; an empty group2 × 1120 px
text, choice, reference, money, quantity, duration, time, date-time; text block, callout, image, data display, custom content, custom field3 × 1180 px
date range, date-time range4 × 1240 px
multiline text, attachment4 × 2240 px

For containers:

  • a group takes its widest child's columns and largest minimum width, with the children's rows added up;
  • a row adds up its cells' minimums (columns capped at 12) and takes the tallest cell;
  • a grid is 12 columns wide and as tall as its row count.

minimumColumnsIn(6, size) halves the column count and rounds up for a 6-column container.

Stacking at fill time ​

FormView keeps layouts readable on narrow screens:

  • A row stacks its cells into one column when its width is below stackBelow, or as soon as any cell would be narrower than its child's minimum width.
  • A grid stacks its items in reading order when any shown item would be narrower than its minimum width.

Changing layout in code ​

Containers have a copyWith for their own properties. RepeatingGroup.copyWith(maxItems:) takes a record so that (value: null) removes the limit.

dart
final roomy = group.copyWith(rowGap: 24, collapsible: true);
final unlimited = materials.copyWith(maxItems: (value: null));
final tight = row.copyWith(columnGap: 8, stackBelow: 720);
final dense = grid.copyWith(rowGap: 8, columnGap: 8);

These return new nodes. To place one in a definition, rebuild the definition, or use the editor controller, which also validates the result and records an undo step:

dart
controller.updateNode('deviation', (node) => (node as FormGroup).copyWith(rowGap: 24));
controller.setRepeatLimits('materials', maxItems: (value: 20));
controller.setRowColumns('material_row', 6);