130 lines
5.0 KiB
Plaintext
130 lines
5.0 KiB
Plaintext
@* Generic modal-per-row list editor. The parent owns the List<TRow> (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
|
|
|
|
<section class="panel rise mt-3" style="@_styleDelay">
|
|
<div class="panel-head d-flex align-items-center">
|
|
<span>@Title (@Items.Count)</span>
|
|
<button type="button" class="btn btn-sm btn-outline-primary ms-auto" @onclick="Add">+ Add @ItemNoun</button>
|
|
</div>
|
|
@if (Items.Count == 0)
|
|
{
|
|
<div style="padding:1rem" class="text-muted">No @ItemNoun.ToLowerInvariant() rows.</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-wrap">
|
|
<table class="data-table">
|
|
<thead>@HeaderTemplate</thead>
|
|
<tbody>
|
|
@for (var i = 0; i < Items.Count; i++)
|
|
{
|
|
var idx = i;
|
|
<tr @key="Items[idx]">
|
|
@RowTemplate(Items[idx])
|
|
<td class="text-end" style="white-space:nowrap">
|
|
<button type="button" class="btn btn-sm btn-link p-0 me-2" @onclick="() => Edit(idx)">Edit</button>
|
|
<button type="button" class="btn btn-sm btn-link p-0 text-danger" @onclick="() => Delete(idx)">Delete</button>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
@if (_modalOpen && _working is not null)
|
|
{
|
|
<div class="modal-backdrop fade show" style="display:block"></div>
|
|
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">@(_editIndex is null ? $"Add {ItemNoun}" : $"Edit {ItemNoun}")</h5>
|
|
<button type="button" class="btn-close" aria-label="Close" @onclick="Cancel"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
@EditTemplate(_working)
|
|
@if (!string.IsNullOrEmpty(_validationError))
|
|
{
|
|
<div class="text-danger small mt-2">@_validationError</div>
|
|
}
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" @onclick="Cancel">Cancel</button>
|
|
<button type="button" class="btn btn-primary" @onclick="Commit">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
[Parameter, EditorRequired] public List<TRow> 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<TRow> RowTemplate { get; set; } = default!;
|
|
[Parameter, EditorRequired] public RenderFragment<TRow> EditTemplate { get; set; } = default!;
|
|
[Parameter, EditorRequired] public Func<TRow> NewRow { get; set; } = default!;
|
|
[Parameter, EditorRequired] public Func<TRow, TRow> Clone { get; set; } = default!;
|
|
[Parameter] public Func<TRow, IReadOnlyList<TRow>, 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();
|
|
}
|
|
}
|