@* Generic modal-per-row list editor. The parent owns the List (a MUTABLE row VM, because driver contracts are immutable records). This renders a read-only table with Add/Edit/Delete and a modal that edits a CLONED working copy — commit on Save, discard on Cancel. NewRow builds a default VM; Clone copies one for the working copy; Validate (optional) returns an error string to block commit or null to allow. *@ @typeparam TRow
@Title (@Items.Count)
@if (Items.Count == 0) {
No @ItemNoun.ToLowerInvariant() rows.
} else {
@HeaderTemplate @for (var i = 0; i < Items.Count; i++) { var idx = i; @RowTemplate(Items[idx]) }
}
@if (_modalOpen && _working is not null) { } @code { [Parameter, EditorRequired] public List Items { get; set; } = default!; [Parameter] public EventCallback ItemsChanged { get; set; } [Parameter] public string Title { get; set; } = "Items"; [Parameter] public string ItemNoun { get; set; } = "row"; [Parameter] public string AnimationDelay { get; set; } = ".18s"; [Parameter, EditorRequired] public RenderFragment HeaderTemplate { get; set; } = default!; [Parameter, EditorRequired] public RenderFragment RowTemplate { get; set; } = default!; [Parameter, EditorRequired] public RenderFragment EditTemplate { get; set; } = default!; [Parameter, EditorRequired] public Func NewRow { get; set; } = default!; [Parameter, EditorRequired] public Func Clone { get; set; } = default!; [Parameter] public Func, int?, string?>? Validate { get; set; } private string _styleDelay => $"animation-delay:{AnimationDelay}"; private bool _modalOpen; private int? _editIndex; private TRow? _working; private string? _validationError; private void Add() { _editIndex = null; _working = NewRow(); _validationError = null; _modalOpen = true; } private void Edit(int index) { _editIndex = index; _working = Clone(Items[index]); _validationError = null; _modalOpen = true; } private async Task Delete(int index) { Items.RemoveAt(index); await ItemsChanged.InvokeAsync(); } private void Cancel() { _modalOpen = false; _working = default; _editIndex = null; _validationError = null; } private async Task Commit() { if (_working is null) return; _validationError = Validate?.Invoke(_working, Items, _editIndex); if (_validationError is not null) return; if (_editIndex is int i) Items[i] = _working; else Items.Add(_working); _modalOpen = false; _working = default; _editIndex = null; await ItemsChanged.InvokeAsync(); } }