# PkRecordForm: the page template of a create-or-edit record page

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from Generated/PkRecordForm.razor and blazor/mappings/record-form.json. Do not edit: change the source and run the generator.

A generated wrapper of the `pk-record-form` element (issue 999), which draws the page: a toolbar, your `Tabs`, the error alert and a `pk-form` (summary of the problems, focus on the first, live checks) around the `<form>` you put in `ChildContent`. The toolbar (Cancel, your `Actions`, Delete, Save) is right-aligned under the page's breadcrumbs (your `PkPageHeader`) and above any tabs; on a phone the buttons fold to their icons. `ChildContent` is your own `<form @onsubmit:preventDefault>` holding the fields (a `PkStack` of `PkCard`s); when `Sidebar` is given the status cards sit beside it, one column on a phone. Cancel shows only with `OnCancel`, Delete only with `OnDelete` (warn variant, disabled while `Busy`). The load, validate and save state is yours, or `PkRecordEditor`'s (`references/record-editor.md`): this draws the page, it does not own the record. Use it for every page that edits one record; for a settings form with no toolbar use `PkForm` and `PkFormActions`.

`OnDelete` is an `EventCallback`, and an unset one (`default`) hides Delete: to offer it only for an existing record keep a field, `private EventCallback _delete;`, assign `_delete = EventCallback.Factory.Create(this, DeleteAsync);` when the record is loaded, and pass `OnDelete="_delete"`.

`ActionsInHeader` drops the toolbar row so the buttons can live in the page's own `PkPageHeader` (`ActionsContent`) and stay in the sticky title bar. Put your own Save button there and have it call `await _form!.SubmitAsync()` on an `@ref` to `PkRecordForm`: pk-form checks the form first and `OnValid` runs when it is valid (a `form` attribute cannot reach into the element). There is no `HeaderActions` property any more.

## Routed list and record page

A list page and a record page are two routes, and the route is the only state. The list navigates on a row click and on Add; the record page loads by the route parameter, draws itself with `PkRecordForm`, and navigates back to the list after Save, Cancel and Delete. This is a full page per record; the `routed-list-detail` template of the `plainkit-sdk` skill (`PkWorkspace`) is the list and record side by side.

```razor
@* Locations.razor: the list *@
@page "/locations"
@inject NavigationManager Nav

<PkPageHeader Crumbs="_crumbs" />
<PkDataTable TItem="Location" Load="LoadAsync" Columns="_columns" IdOf="l => l.Id.ToString()" Label="Locations"
             AddLabel="+ Add location" OnAdd="Add" OnRowClick="Open" />

@code {
    private readonly PkCrumb[] _crumbs = [new PkCrumb("Locations")];
    private readonly IReadOnlyList<PkTableColumn<Location>> _columns = [new() { Key = "name", Label = "Name" }];

    private void Add() => Nav.NavigateTo("/locations/new");
    private void Open(Location location) => Nav.NavigateTo($"/locations/{location.Id}");
    // LoadAsync as in the server-paged list workflow of the skill.
}
```

```razor
@* LocationEdit.razor: the record page, one component for both routes *@
@page "/locations/new"
@page "/locations/{Id:int}"
@inject NavigationManager Nav
@inject ILocationRepository Repo
@inject ILogger<LocationEdit> Log

<PkPageHeader Crumbs="_crumbs" Title="@_title" />
<PkRecordForm OnValid="SaveAsync" OnCancel="Back" OnDelete="_delete" Busy="_busy" Error="@_error" SaveLabel="Save location">
    <form @onsubmit:preventDefault>
        <PkStack>
            <PkCard Heading="Details">
                <PkField Label="Name" Required>
                    <PkInput @bind-Value="_form.Name" Name="name" Required />
                </PkField>
            </PkCard>
        </PkStack>
    </form>
    <Sidebar>
        <PkCard Heading="Status"><PkBadge>@_status</PkBadge></PkCard>
    </Sidebar>
</PkRecordForm>

@code {
    [Parameter] public int? Id { get; set; }

    private LocationForm _form = new();
    private EventCallback _delete;      // unset means no Delete button: only an existing record has one
    private bool _busy;
    private string? _error;
    private string _title = "New location";
    private string _status = "New";
    private readonly PkCrumb[] _crumbs = [new PkCrumb("Locations", "/locations"), new PkCrumb("Location")];

    protected override async Task OnParametersSetAsync()
    {
        _error = null;
        if (Id is not { } id)
        {
            _form = new LocationForm();
            _delete = default;
            return;
        }
        var location = await Repo.GetAsync(id);
        if (location is null) { Nav.NavigateTo("/locations", replace: true); return; }
        _form = LocationForm.From(location);
        _title = location.Name;
        _status = "Saved";
        _delete = EventCallback.Factory.Create(this, DeleteAsync);
    }

    private Task SaveAsync() => RunAsync(() => Repo.SaveAsync(Id, _form));
    private Task DeleteAsync() => RunAsync(() => Repo.DeleteAsync(Id!.Value));
    private void Back() => Nav.NavigateTo("/locations");

    // Busy while it runs, back to the list on success, a fixed line in the form's alert on failure (the exception goes to the log).
    private async Task RunAsync(Func<Task> action)
    {
        if (_busy) return;
        _busy = true;
        _error = null;
        try { await action(); Back(); }
        catch (Exception ex) { Log.LogError(ex, "Location save or delete failed"); _error = "Could not complete that. See the log for details."; }
        finally { _busy = false; }
    }
}
```

`PkForm` blocks a submit the browser's own validation rejects (its summary lists why), so `OnValid` only runs for a valid form. A caught exception's text is not for the user: log it and show a fixed line, unless it is your own domain error meant to be shown. Give the list a `CurrentId` when the record page is shown beside it. The alert, toolbar and layout come with the component: do not add another error `PkAlert` or a `PkFormActions` inside it.

## `PkRecordForm`

| Parameter | Type | Description |
|---|---|---|
| `SaveLabel` | `string?` | The Save button's text. Change it only for a page whose submit is an action ("Record payout"). |
| `DeleteLabel` | `string?` | The Delete button's text. |
| `BusyText` | `string?` | The Save button's text while busy. |
| `SaveDisabled` | `bool` | Disables Save (a locked record, or nothing to save yet). Independent of busy. |
| `Busy` | `bool` | Save shows its busy state and Delete is disabled while the page saves or deletes. |
| `Error` | `string?` | A message shown in an error alert above the form; empty shows none. |
| `TabsFrom` | `RecordFormTabsFrom?` | Where the tabs in the tabs slot take effect. An element in the form or the sidebar with a tab attribute (a tab value, or several separated by spaces) belongs to those tabs: at or below the width named (phone, tablet), or always, only the chosen tab's sections show and the strip is visible; above it every section shows (the two-column layout) and the strip is hidden. Sections with no tab attribute always show. The element sets hidden on the tagged sections. |
| `TabParam` | `string?` | A query parameter name ("tab") that keeps the chosen tab in the address bar, so a reload or a shared link opens on it. Other parameters and the hash are kept and no history entry is added. Empty keeps nothing. A value that matches no enabled tab is ignored. |
| `TabItems` | `IReadOnlyList<PkRecordTab>?` | The tabs as data: an array of { value, label, count?, visible? }. The element draws the strip (a pk-tabs in the tabs slot wins); count is the badge after the label, and a tab with visible false is left out (a chosen tab that went falls back to the first). Sections tag themselves with tab="value" as with tabs-from. Empty draws nothing. |
| `ActionsInHeader` | `bool` | Leaves the toolbar row out so the buttons can sit in the page's own header and stay visible in its sticky title bar. Put your own Save button in the header and have it call submit() (a form attribute cannot cross this element's shadow root). |
| `OnValid` | `EventCallback` | Save was pressed (or submit() called, or the form submitted) and pk-form found the form valid: save the record. An invalid form raises no event; pk-form shows the problems. |
| `OnCancel` | `EventCallback` | Cancel was pressed. |
| `OnDelete` | `EventCallback` | Delete was pressed. |
| `ChildContent` | `RenderFragment?` | Your own form element, with its fields. pk-form wraps it. |
| `Sidebar` | `RenderFragment?` | Status and flag cards beside the form (pk-detail-layout). Left empty, the form takes the whole width. |
| `Actions` | `RenderFragment?` | Extra toolbar buttons, between Cancel and Delete. |
| `Tabs` | `RenderFragment?` | A pk-tabs strip for a record with more than one job, under the toolbar. Tag sections with a tab attribute (see tabs-from) and the chosen tab shows its sections, or render the chosen tab's content yourself. |
