Diagram
100%

Drag to pan · Scroll to zoom · + / − to zoom · 0 to fit · Esc to close

Skip to content

Layout ​

The layout tree starts at the definition's layout, a FormLayout. Containers nest: the layout, 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.
maxRowCells6Most cells in one row.
defaultStackBelow600.0Width in px below which a row stacks.
GridCellSizesmall (32), medium (48), large (64)Side of a grid's square cells, in px; each grid has its own cellSize, medium by default.

Containers ​

NodePropertiesAt fill time
FormLayoutid, 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 (at least 1), rows (at least 1), placements (GridPlacement(child, x, y, width, height)), cellSize, rowGap, columnGapItems on fixed square cells of the grid's cellSize, across the container's full width.

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 span in that grid.

Every cell is a square of the grid's cellSize on each side, columns and rows alike: 32, 48 (the default) or 64 px. The grid's gaps sit between cells, so an item's footprint at 48 px cells is:

  • width = width × 48 + (width − 1) × columnGap
  • height = height × 48 + (height − 1) × rowGap

A grid spans the full width of its container; its cells never stretch. columns is how many columns the author designed against: as many cells as fit the designer's canvas when the grid was last changed. rows is as many as its placements reach. cellSize.spanExtent(span, gap) and cellSize.offsetOf(index, gap) give these lengths and positions; gridWidthOf(grid), gridHeightOf(grid) and gridColumnsFitting(grid, width) measure a whole grid.

Changing a grid's cells (setGridCells(grid, fields, cellSize:, rowGap:, columnGap:, columns:)) takes exactly the columns given, never widening the grid, and keeps every placement's cells where it can. An item that would then be smaller than its minimum grows right and down to it, one wider than the columns narrows to them, and one that would reach past the last column shifts left; the change is refused, naming the item, when one would then cover another. The designer passes the columns that fit its canvas at the new cells.

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

Steps ​

A form is one step by default. Give its layout FormSteps as its only children (two or more) and a FormStepper, and FormView fills it as a wizard. A single-step form has no steps and no stepper in its JSON, so every existing definition reads as it did, and formSchemaVersion stays 4.

dart
form('change_control', 'Change control', [
  step('basics', [title, owner], title: 'Basics'),
  step('details', [reason], optional: true),
  step('approval', [approver], title: 'Approval'),
], stepper: const FormStepper(
  orientation: FormStepperOrientation.top,
  navigation: FormStepNavigation.linear,
  review: true,
));

A step holds what a group holds, and sits only directly under the layout: never inside a group, a row, a grid or another step. A step's title is its name in the stepper; without one it reads by its place ("Step 2"), so reordering renumbers it. An optional step can be skipped.

FormStepper says how the steps are drawn and walked:

SettingValues
orientationtop (a bar with one segment per step, over each step's name and status), side (a vertical list), compact ("Step 2 of 4 · Name" over a progress bar)
navigationfree (any step can be jumped to) or linear (only the steps up to the furthest reached)
reviewtrue ends the form with a generated, read-only "Review and submit" step with an Edit link per step
actionswhether each of Back, Skip and Next shows, and what it says. Next always shows. Cancel, Save draft and Submit belong to every form, one step or several: see Actions

The rules work across steps: a rule can show, hide, enable or disable a whole step, and a step whose fields are all hidden is skipped over as not applicable.

To change the steps in code, use addStep, removeStep, moveStep, renameStep, setStepOptional and setStepper. Adding the second step wraps the elements into "Step 1"; removing down to one unwraps them and drops the stepper settings; removing a step takes its fields and the rules that use them.

Minimum sizes ​

minimumSizeOf(node, fields) returns a FormElementSize(columns:, minWidth:, minHeight:): a column count out of 12, and the pixel width and height the element needs. The widths are whole cells at the default gap: 112 px is 2 cells, 176 px is 3 and 240 px is 4. The heights are measured from a field's grid cell (label, control and the reserved message line) at the default theme, at the element's minimum width. The model refuses spans and placements below the minimum, the editor clamps resizes to it, and FormView uses minWidth to decide when a row stacks.

ElementColumnsMin widthMin height
number, date-only2112 px86 px
formula2112 px74 px
toggle2176 px (its switch wraps under the label below that)72 px
rating240 px per star74 px
text, choice, reference, money, quantity, duration, time, date-time3176 px86 px
custom field3176 px86 px, assumed: a plugin draws the control, so this is not measured
date range, date-time range4240 px86 px
attachment4240 px74 px
signature4240 px94 px (its signed receipt)
multiline text4240 px150 px
divider2112 px1 px
spacer2112 px8 px per grid unit
image3176 px20 px (in a grid it fills its placement)
callout3176 px80 px: a title and two lines of body
custom content3176 px40 px, assumed like a custom field

For containers:

  • a group takes its widest child's columns and largest minimum width; its height stacks its title line (24 px, or 56 px for a collapsible header) and its children with its rowGap between them. An empty group is 2 columns and 112 px wide;
  • 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 cells.

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

Minimum spans in a grid ​

minimumGridSpanOf(node, fields, grid) turns a minimum size into the smallest (columns, rows) span the node can take in that grid, at the grid's own cell size and gaps. The same function backs the validator, the grid rules, drops and the editor.

  • columns = ⌈(minWidth + columnGap) / (cellSize + columnGap)⌉, and never more than the grid's columns
  • rows = ⌈(minHeight + rowGap) / (cellSize + rowGap)⌉

At 48 px cells and the default 16 px gaps a text field needs 3 × 2 cells, a number or date 2 × 2, a toggle 3 × 2, a multiline text 4 × 3, a callout 3 × 2 and an image 3 × 1. At 32 px cells a text field needs 4 × 3, and at 64 px 3 × 2. A narrower gap can raise a span: with no gap a text field needs 4 columns of 48 px.

An element wider than the whole grid, such as a 10-star rating (400 px) in a 6-column grid of 48 px cells (368 px), takes every column and its content clips, just as content taller than its rows does. gridSpanOf(size, grid) does the same for a size you already have.

Wrapping at fill time ​

FormView keeps layouts readable on narrow screens. What doesn't fit wraps item by item; what can't wrap any further clips.

  • Items keep their designed places while they fit. The first item whose span doesn't fit horizontally wraps to the next line, with the items after it, in reading order. Items that fit are never moved because a later one didn't.
  • A single field or piece of content wider than the room, alone on its line, can't wrap any further: it keeps its width and is clipped on the right, never overflowing. A container (a group, repeating group, row or grid) too wide for the room isn't clipped: it takes the room, and its children wrap by these same rules.
  • A row lays its cells out at their spans while each is at least its child's minimum width. Otherwise its cells shrink to their minimum widths and fill lines in order, the first that doesn't fit starting the next line, and each line's spare width is shared by span. Below its stackBelow width, every cell takes a line of its own.
  • A grid spans its container's full width on square cells of its cellSize. When every shown item fits the columns there are, each sits at its placement and the grid is its designed rows tall, less the rows hidden items alone held (see below). Otherwise the items before the first that doesn't fit keep their placements, and it and the items after it wrap onto lines below, each keeping its size in cells; one wider than all the columns is alone on its line, cut to them.

Hidden elements take no room ​

At fill time (in FormView, and so in the designer's preview) what rules hide takes no space anywhere. One rule decides it, effectivelyShown(node, isShown): a node is shown when its own rules show it, and a container also needs something shown inside it. FormRenderContext.isEffectivelyShown(node) asks it for the current values, and every container lays out only what it holds for.

  • A flow (the form, a group or an entry) leaves out what isn't shown, with no double gap.
  • A row drops its hidden cells. The shown cells share the room the hidden ones had (each hidden cell's span and the gap after it) by span, so the line stays full; the wrap and minimum-width rules above then apply to the shown cells only.
  • A grid's hidden items free their cells. A row that some hidden item covered and no shown item covers collapses, and every item below moves up by the rows that collapsed. Nothing moves sideways, so the design keeps its columns, and rows the author left empty stay. The per-item wrap then works on the items left.
  • A group, repeating group, row or grid with nothing shown inside is hidden itself: its title, border and gap all go. A repeating group's entry card goes the same way when its entry's rules hide everything in it; the others keep their numbers.
  • A spacer or divider only separates what is around it, so alone it doesn't keep a container shown. A callout, image or custom content does.

The fields in a container that goes are already hidden, every one of them, so validation skips them and submitted leaves them out, just as it does for any hidden field.

The designer's canvas is for editing, not filling: it keeps every element at its designed place, marked with the bolt of the rules that change it.

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);