🙌

動的に入力項目を増やしたり出来るフォームを Blazor で作ってみよう

2022/12/07に公開

先日 .NET Conf 2022 Recap Event Tokyo の Ask the speaker で、動的に入力項目が変わる入力フォームを作る方法を聞かれました。その際に Dictionary などを使って動的に項目が増えるようなクラスを作って、それを元にフォームを組み立てたりするとうまくいくのかもしれないと回答したのですが、やったことはなかったのでやってみました。

動的に入力項目が増えるフォームを表すクラスを定義する

ということでやっつけで以下のようなクラスを定義しました。ラベルと値を持ったクラスを定義して、それのリストとしてフォームに表示する項目を表すようにしました。

using System.Text.Json;

namespace BlazorApp3;

public class DynamicForm
{
    private readonly List<FormItem> _formItems = new();

    public void AddFormItem(FormItem item) => _formItems.Add(item);
    public IEnumerable<FormItem> FormItems => _formItems;
    public override string ToString() => JsonSerializer.Serialize(_formItems);
}

public record FormItem
{
    public string Label { get; set; } = default!;
    public string? Value { get; set; }
}

そして Index.cshtml を以下のようにすれば完成です!

Index.cshtml
@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

<EditForm EditContext="_editContext" OnInvalidSubmit="InvalidSubmit" OnValidSubmit="ValidSubmit">
    @* フォームの項目をループして回して作る *@
    @foreach (var item in _form.FormItems)
    {
        <div>
            <label>
                @item.Label:
                <InputText @bind-Value="item.Value" />
                <ValidationMessage For="() => item.Value" />
            </label>
        </div>
    }

    <div>
        <button class="btn btn-primary" type="submit">Submit</button>
    </div>
</EditForm>

<div>@_resultMessage</div>

<div>
    @* 項目の追加処理用画面 *@
    <h3>項目を追加</h3>
    <input @bind="_newItemLabel" />
    <button @onclick="OnAddFormItemClick">Add</button>
</div>

<div>
    @* フォームの中身を確認するために JSON 形式で出力 *@
    <h3>Form data</h3>
    @_form
</div>

@code {
    private readonly DynamicForm _form = new();
    private string _newItemLabel = "";
    private EditContext _editContext = default!;
    private ValidationMessageStore _validationMessageStore = default!;

    private string _resultMessage = "";

    protected override void OnInitialized()
    {
        // 初期状態で2項目作っておく
        _form.AddFormItem(new() { Label = "Name" });
        _form.AddFormItem(new() { Label = "Age" });

        // EditContext やバリデーションのメッセージ保存場所やバリデーションのトリガーのイベントを購読
        _editContext = new(_form);
        _validationMessageStore = new(_editContext);
        _editContext.OnValidationRequested += ValidationRequested;
    }

    private void OnAddFormItemClick()
    {
        // フォームに項目を追加
        if (string.IsNullOrWhiteSpace(_newItemLabel)) return;

        _form.AddFormItem(new() { Label = _newItemLabel });
        _newItemLabel = "";
    }

    private void ValidationRequested(object? sender, ValidationRequestedEventArgs args)
    {
        // バリデーション
        _validationMessageStore.Clear();

        foreach (var item in _form.FormItems)
        {
            if (!string.IsNullOrWhiteSpace(item.Value)) continue;
            // とりあえず全部一律必須入力にする
            _validationMessageStore.Add(() => item.Value!, $"{item.Label} is required");
        }
    }

    private void InvalidSubmit(EditContext editContext)
    {
        // Valid!!
        _resultMessage = $"{DateTime.Now}: InvalidSubmit";
    }

    private void ValidSubmit(EditContext editContext)
    {
        // Invalid!!!
        _resultMessage = $"{DateTime.Now}: ValidSubmit";
    }
}

初期状態では Name と Age を持っただけのフォームが表示されます。

必須入力チェックを実装しているので未入力状態で Submit ボタンを押すとエラーが表示されます。

項目を追加していくと以下のように項目が追加されます。

もちろん必須入力チェックも走ります。

ちゃんと値を入れると ValidSubmit のイベントが呼ばれています。そしてちゃんと DynamicForm クラス内にも入力した内容が表示されています。

いい感じ。

まとめ

ということで、動的に項目が増えるフォームが実装できました。実際に使う場合は型に応じて表示するコンポーネントを切り替えたり (.NET 6 で動的にコンポーネントをレンダリングする機能が追加されたので試してみる)、バリデーションは必須入力だけではなく独自ロジックにしたいなどあると思います。

ここら辺は FormItem クラスに表示に使いたいコンポーネントの型やバリデーションロジックを持たせるように拡張することで実装していけると思います。あとは、現在はフォームの Submit ボタンを押したときにバリデーションが走っていますが、テキストボックスからフォーカスが外れたタイミングでバリデーションを行いたいといったこともあると思います。
そこは値の変更時にバリデーションをして ValidationMessageStore の中身を書き換えてあげればいけるような気がします。

とりあえず動きそうなことがわかったので今日は満足。

Microsoft (有志)

Discussion