# PkFieldGroup: a plain field bound to a model property, from a list of specs

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from Components/PkFieldGroup.razor and PkFieldGroupTypes.cs. Do not edit: change the source and run the generator.

A hand-written component with no element of its own. Renders a `PkField` plus the right control (`PkInput`, `PkSelect`, `PkTextarea` or `PkCheckbox`) for each `PkFieldSpec<TItem>`, reading and writing `Model` through the spec's `Get`/`Set` (`Func<TItem, string?>`/`Action<TItem, string?>`: every control's own value-shaped parameter is a string, so a field spec deals in the same currency; `PkInput.Min`/`Max`/`Step`/`MaxLength` are strings for the same reason). A field can gate its own rendering with `When` (`Func<TItem, bool>`), re-evaluated on every render; a field whose value is computed rather than a plain mirror still stays hand-written next to it, the same `PkField` plus control you would already write. Composes with `PkForm`'s own validation with no extra wiring: every control it renders is the same real element a hand-written field would use, and a field hidden by `When` renders no markup at all, so a hidden `Required` field never blocks submission.

```razor
<PkForm>
    <PkFieldGroup TItem="Order" Fields="_fields" Model="_order" ModelChanged="Save" />
</PkForm>

@code {
    private Order _order = new();
    private IReadOnlyList<PkFieldSpec<Order>> _fields =
    [
        new() { Key = "name", Label = "Name", Required = true, Get = order => order.Name, Set = (order, v) => order.Name = v ?? "" },
        new() { Key = "qty", Label = "Quantity", Kind = PkFieldKind.Number, Min = "1", Get = order => order.Qty.ToString(), Set = (order, v) => order.Qty = int.Parse(v ?? "0") },
        new() { Key = "status", Label = "Status", Kind = PkFieldKind.Select, Options = [new("open", "Open"), new("closed", "Closed")], Get = order => order.Status, Set = (order, v) => order.Status = v ?? "" },
        new() { Key = "note", Label = "Closing note", Required = true, When = order => order.Status == "closed", Get = order => order.Notes, Set = (order, v) => order.Notes = v ?? "" },
    ];
}
```

## `PkFieldGroup`

| Parameter | Type | Description |
|---|---|---|
| `Columns` | `int?` | How many columns the fields flow into, up to as many as fit (a column is at least 16rem wide, so a narrow group and a phone show one); a field with span fills the row and one with columnSpan takes that many columns. |
| `Layout` | `FieldGroupLayout?` | grid (the form-section grid: columns, span and cols apply) or inline: the fields sit in one wrapping row, bottom-aligned with the content of the end slot, for a short add-row. A field is 12rem wide (shrinking) unless it has cols (1 to 12), which sizes it as twelfths of the row exactly as in the grid (never under 5rem), so a short quantity stays short beside a wide picker. span, columnSpan and columns are ignored inline. Bottom alignment is of the whole field, so a hint under one field lifts its control: keep hints out of an add-row. |
| `EndContent` | `RenderFragment?` | Trailing content on the fields' row with layout="inline" (the Add and Cancel buttons of an add-row), bottom-aligned with the controls. |
| `Fields` | `IReadOnlyList<PkFieldSpec<TItem>>` | The fields to render, in order. |
| `Model` | `TItem` | The bound record; each field's Get reads it, each commit calls Set then ModelChanged. |
| `OnFieldAction` | `EventCallback<PkFieldActionEventArgs>` | The user pressed a field's suffixAction button: the field's key and the action's key. |
| `ModelChanged` | `EventCallback` | Raised after a field commits (the model was updated through Set, or SetAsync succeeded). A handler on the parent re-renders it and this group: Get, When, Disabled and HelpWhen are read again. |

## `PkFieldSpec<TItem>`

One field: `Key`, `Label`, `Get` (`Func<TItem, string?>`), `Set` (`Action<TItem, string?>`), `Kind` (`PkFieldKind`, default `Text`), `Hint`, `Required`, `Min`, `Max`, `Step`, `MaxLength`, `Pattern`, `Placeholder` (`PkInput`/`PkTextarea`), `Rows` (`int?`, `Textarea` only), `Help` (text in a help `PkTooltip` beside the label), `Options` (`IReadOnlyList<PkFieldOption>`, `Select` only), `Key` is also emitted as each control's `name`. For a `bool` property use `PkFieldSpec<TItem>.Bool(key, label, item => item.Active, (item, v) => item.Active = v)` (a `Checkbox` spec wrapping the string `Get`/`Set`; add other members with `with { ... }`). `When` (`Func<TItem, bool>`, unset means always rendered; re-run for every field on every render, since one field's value can gate another's visibility).

`Disabled` and `ReadOnly` (`Func<TItem, bool>`, per render: a code editable while the record is new and locked after; `ReadOnly` keeps the value selectable, on `PkSelect`/`PkCheckbox` it disables), `Span` (`bool`: the field takes every column of the `pk-form-section` grid, the `form-span` class, and the whole row of a `PkFieldGroup` with `Columns`), `ColumnSpan` (`int?`, 2 or 3: the field takes that many columns of a `PkFieldGroup` with `Columns="3"`, one column when the group is narrow; set `Columns` on the group so short related fields share a row, one column on a phone), `Cols` (`int?`, 1 to 12: the width in twelfths of the row; one field with it puts the group on a 12-column grid where short fields share a row and wrap, a field without it fills the row, two small fields per row on a phone), `OptionsSource` (`Func<IReadOnlyList<PkFieldOption>>`, read at every render, wins over `Options`; for lists that load later), `HideLabel` (`bool`: the label is not shown but is the control's accessible name) `HelpWhen` (`Func<TItem, string?>`: help per record, used instead of `Help` when non-empty) and `LabelWhen` (`Func<TItem, string?>`: the label per record, e.g. one naming another field's value; used instead of `Label` when non-empty) `SuffixAction` (`PkFieldAction?`: `new("search", "Find SKU", "find")` draws an icon button inside the end of an input control, `Icon` an icon name, `Label` its accessible name; it follows `Disabled` and `ReadOnly`, and a press raises `OnFieldAction` on the group (`PkFieldActionEventArgs`: `Key` of the field, `Action` of the button); other kinds ignore it) and `LabelAction` (`RenderFragment<TItem>`: extra content in the label-action slot beside the label, after the Help tooltip and re-evaluated each render; for adopt-from-source buttons and flag badges, on every kind including Custom; `LabelActionEnd` (bool) pins it to the end of the label row; never replaces the label text). The help button's accessible name is "Help for <Label>"; for a `Checkbox` the tooltip sits beside the field label, like any other field. `Format`, `Decimals`, `Currency` and `Locale` (on a number input: `Format = "money"` shows currency while the value stays the plain number). `Editor` (`RenderFragment<TItem>`, built with `PkFieldSpec<TItem>.Custom(key, label, o => ...)`: your own content in the field body instead of a control, for an input with an inline action or a read-only summary; no value, no part in the form or validation, `Get`/`Set` never called). `PkFieldSpec<TItem>.Decimal(key, label, get, set, min, max, step)` is a number spec over a `decimal?` property with invariant-culture text for the value and for `Min`/`Max`/`Step`. A text field that suggests values but accepts any text is `Kind = PkFieldKind.Combobox` with `Free = true` and `Options`. A change that must call a service or reload dependent data is `PkFieldSpec<T>.Async(key, label, get, async (o, v) => ...)` (`SetAsync`): the field shows busy while it runs, a newer edit of the field replaces a pending one (last wins; a stale completion changes nothing), an exception shows its message as that field's error and keeps the typed text, and `ModelChanged` is raised only after it succeeds (the element does this through its `commit` callback; in plain JS set `el.commit = ({ key, value, values }) => promise`, a rejection or a returned string is the error).

`ModelChanged` is raised after `Set` has updated the model. It is an `EventCallback`, so a handler on the parent re-renders the parent and the group with it; give the group a handler (even an empty method) when other UI on the page depends on a field.

## `PkFieldKind`

Text, Number, Email, Password, Date, DateTime (`type="datetime-local"`: a date and a time of day with no time zone, the value text `yyyy-MM-ddTHH:mm`; use `PkFieldSpec<TItem>.DateTime(key, label, get, set, min, max)` over a `DateTime?` property, which reads and writes that text in invariant culture and sets null for an empty or unreadable one; a `DateTimeOffset` or UTC property is converted in your own `Get`/`Set`, the field never shifts a zone), Time, Url, Tel (all `PkInput`, `Type` set from the member), Textarea, Select, Checkbox (`Get`/`Set` still deal in strings: checked is a non-empty value other than "false"), Combobox (a `PkCombobox`: options from `Options`/`OptionsSource` or, as the user types, the async `Search` callback `Func<string, Task<IReadOnlyList<PkFieldOption>>>` (the element then does not filter them again; a newer query supersedes an older pending one); `Free` accepts the typed text as the value).
