💻

knockout.js の observableArray でクライアントサイド検証を有効にする

2022/01/01に公開

ko.observableArray でバインドしたフォーム要素にバリデーションを入れたかったのですが、普通にやっただけではうまく動いてくれないようです。knockout.js のプラグインには Knockout Validation というのもあるようなのですが、以下の理由で断念してしまいました。

  • そもそも ko.observableArray に対応してない (それぞれの要素に ko.observable する必要がある)
  • jquery.validate.unobtrusive.js に対応してない (致命的)

「誰も作らないなら自分で作るしかない」ということで、knockout.js のカスタムバインディングを作ってみました。

$(function () {
    ko.bindingHandlers.validate = {
        update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            if (valueAccessor()) {
                $.validator.unobtrusive.parse(element);
            }
        }
    };
});

$.validator.unobtrusive.parse メソッドに要素を指定すると、動的に追加したフォームの検証を有効にしてくれるらしいです。あとは、バインドしてあげれば完成です。

@{
    ViewBag.Title = "ホーム";
    Html.EnableClientValidation();
    Html.EnableUnobtrusiveJavaScript();
}
<form action="/" method="post">
    <input type="button" value="追加" data-bind="click: add" />
    <input type="button" value="送信" data-bind="click: submit" />
</form>
<fieldset id="Items" data-bind="foreach: items">
    @using (Html.BeginForm("Index", null, FormMethod.Post,
        new Dictionary<string, object>() { { "data-bind", "validate: true" } })) {
        @Html.ValidationSummary()
        <table>
            <tbody>
                <tr>
                    <td class="editor-label">
                        <span>名前:</span>
                    </td>
                    <td class="editor-field">
                        <input id="Name" name="Name" type="text" data-bind="value: name" data-val="true" data-val-required="名前は必須です。" />
                    </td>
                    <td class="editor-label">
                        <span>年齢:</span>
                    </td>
                    <td class="editor-field">
                        <input id="Age" name="Age" type="text" data-bind="value: age" data-val="true" data-val-number="年齢は数値です。" />
                    </td>
                </tr>
            </tbody>
        </table>
    }
</fieldset>
<script type="text/javascript">
    function ViewModel() {
        this.items = ko.observableArray([]);
        this.add = function () {
            this.items.push({ name: null, age: null });
        };
        this.submit = function () {
            var valid = Enumerable
                .From($("#Items").find("form"))
                .Select(function (form) { return $(form).valid() })
                .All(function (result) { return result });
        };
    };
    ko.applyBindings(new ViewModel());
    $(function () {
        ko.bindingHandlers.validate = {
            update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
                if (valueAccessor()) {
                    $.validator.unobtrusive.parse(element);
                }
            }
        };
    });
</script>

Discussion