Skip to content

Array / repeater

Array fields render a repeating editor with add, remove and reorder. Each item is a nested object validated like any other field, and its errors travel with it when rows move. The Total below is a computed field: the schema derives it from the lines, so no page code keeps it in sync.

Try it with the keyboard. Adding a line puts the caret in it; removing one moves the caret to the line that took its place — rather than dropping it on the page body, which is where a button that deletes itself normally leaves you. An invoice needs at least one line, so the last one cannot be removed; on a repeater that can be emptied, the caret goes to the add button.
Live form
New invoice

Lines

No lines yet.

Disabled makes the form inoperable — every control and button, the repeater's buttons included. It is deliberately not the same as ReadOnly, which keeps the fields focusable and readable for a review screen.

View source
@page "/array"
@implements IDisposable

<BormView State="_state" OnValidSubmit="HandleSubmit" SubmitText="Save invoice" ShowErrorSummary="true" />

@code {
    private readonly Invoice _model = new();
    private BormState _state = default!;

    protected override void OnInitialized()
    {
        var schema = BormBuilder.For<Invoice>()
            .Title("New invoice")
            .Field(x => x.Customer, f => f.Required().Autocomplete("organization"))
            .Array(x => x.Lines, line => line
                    .Field(l => l.Description, f => f.Required())
                    .Field(l => l.Quantity, f => f.Range(1, 9999))
                    .Field(l => l.UnitPrice, f => f.Range(0, 1_000_000).Step(0.01))
                    // Each line's total is derived from its own quantity and price.
                    .Computed(l => l.LineTotal, l => l.Quantity * l.UnitPrice,
                              dependsOn: ["Quantity", "UnitPrice"]),
                f => f.Items(min: 1, message: "Add at least one line.").Attr("itemNoun", "line"))
            // ...and the invoice total from every line. Recomputed whenever anything under Lines changes.
            .Computed(x => x.Total, m => m.Lines.Sum(l => l.Quantity * l.UnitPrice),
                      dependsOn: [nameof(Invoice.Lines)])
            .Build();

        _state = new BormState(schema, new BormModelDataAccessor(_model));
        _state.StateChanged += OnStateChanged;
    }

    private void OnStateChanged() => InvokeAsync(StateHasChanged);

    public void Dispose()
    {
        _state.StateChanged -= OnStateChanged;
        // The page created the state, so the page disposes it: an options load or async rule still in
        // flight is cancelled instead of finishing against a form nobody is looking at.
        _state.Dispose();
    }
}
An unhandled error has occurred. Reload 🗙