鶏口牛後な日々

心の赴くまま、やりたいことを仕事に。

PHP Laravelのアップデート画面でcheckboxを操作する

チェックボックスやセレクトボックスの制御は、色々と便利なメソッドが用意されているLaravelでも案外面倒

PHP Laravelで、一度登録した情報の編集画面を作るときに、checkboxやセレクトボックスの挙動を指定するのが、思った以上に面倒だったので、メモしておきます。

実現したいこと

実現したいのは、以下です。

  1. 編集画面を開くと、登録した内容が表示される
  2. バリデーションでリダイレクトしたとき、変更が反映された状態で表示される

(おさらい)ただのinputタグだったら無問題

ただの input type="text" や、 input type="number" などであれば、とても便利な old() という関数が用意されています。 これをbladeに埋め込むことで、実現したいことの2つともが実現できます。

ここで、 $cow はviewを呼び出す際に、Controller側でセットされて渡される、以前登録した情報だとします。

<input type="text" name="cow_name" value="{{ $cow_name }}"> 

これで、実現したいこと1はOK。さらに、 old を使います。

<input type="text" name="cow_name" value="{{ old('cow_name', $cow_name) }}">

old() メソッドは、前回submitしたときの値を取ってくれて、第一引数に、取ってきたいinputの name 属性を入れ、第二引数には、デフォルト値を入れます。 こうすることで、デフォルトには、最初にControllerから渡される値を入れ、リダイレクトされたら、前回submit時に入れていた値を取得して入れてくれるというわけです。

とても便利な関数だな、と思います。

(ここから本番)checkboxの場合

しかし、チェックボックスは、value 属性がないため、上記のような書き方ができないのです。順番に見ていきます。

<input type="checkbox" name="chbox">

チェックボックスでデフォルトでチェックされた状態にする

デフォルト値を入れたい場合は、以下のように書きます。

<input type="checkbox" name="chbox" checked>

では、次の段階として、Controllerから渡された状態に合わせてチェックする・しないを指定します。

Controllerから渡された値をもとにチェックする・しないを指定する

$checked はControllerでセットされた値(=チェックされたかされないか)だとします。

<input type="checkbox" name="chbox" @if( $checked == true ) checked @endif>

これで、実現したいことの1つ目だけであれば実現できます。

さらに、(他の項目が)バリデーションに引っかかって、リダイレクトされた時に、変更した操作を反映したい場合はどうするかです。

リダイレクト時に、リダイレクト前に操作した内容をもとにチェックする・しないを指定する

<input type="checkbox" name="chbox" @if( $checked == true ) checked @endif @if( old('chbox') == true ) checked @endif>

書いて見ましたが、これではダメです!

なぜなら、編集画面を開いて、チェックを外した状態で、リダイレクトされた場合、外した状態がこれでは反映されません。

本当は、2つ目の @if @endif ディレクティブの中で、@if( old('chbox') == true) checked @else checked="false" @endif> とでもしたいところですが、これは残念ながらできません。

というのも、 checked 属性は、外した状態の指定ができないからです。

MDN公式の説明にも以下のように書かれています。

Note: If a checkbox is unchecked when its form is submitted, there is no value submitted to the server to represent its unchecked state (e.g. value=unchecked); the value is not submitted to the server at all.

訳↓
注意:チェックボックスがチェックされない状態で送信されると、サーバーには「チェックされない状態」を表す値(例: value=uncheked)といったものは送信されません。サーバーには何も送信されません。

つまり、 inputタグにおいて、 checked 属性は、チェックされた状態は指定できるけど、されない状態を指定することができないのです。 そうなると、blade上で、 @if ディレクティブを使って実装するのは困難になります。

じゃあどうするの?

Javascriptを使いました

といっても結構簡単で、しかもスッキリ書けます。

bladeファイルの head タグ内に、script タグを用意して、 @jsonold() メソッドの値を取得し、それに応じてjQueryでチェックするかしないかを指定します。

<head>
    <script>
        // リダイレクト前に選択していた値を受け取る
        var oldChecked = @json( old('chbox');

        $(document).ready( function() {
            // チェックボックスにセットする
            var cb = $('input[name="chbox"]');
            if ( oldChecked == 'on' ) {
                cb.prop('checked', true);
            } else {
                cb.prop('checked', false);
            }
        });
    </script>
</head>

「checkedされていた」場合、 old('chbox') で入る値は、 on です。 ですので、 on であれば、チェックをつけ、そうでなければ、チェックを外す、という操作をします。 javascript(jQuery)であれば、「チェックをつけていない状態」に操作ができるというのは助かりますね。

以下いくつかポイントです。

  • ページが読み込まれた後にこのscriptを動作させないといけないので、 $(document).ready(function() {}); で囲ってやるのを忘れないようにします

  • jQueryでは prop() を使っていますが、 attr() でもできそうですが、 prop を使ったほうが良さそうです。こちら(英語ページ)で説明されていますが、attr() はHTMLの属性を変えることができるが、 prop() は、HTMLがロードされ後に作られるDOMのプロパティを変えるメソッドだからです。なので、HTMLを読み込んだ後に、いくつもjavascriptで操作した後だった場合にも、そのjavascriptが反映された状態で、操作をしてくれるというわけだと思われます

というわけで、実現したいこと2つ目もこれで実現できました。

もちろん、実現したいこと1つ目もjsで書いてしまうこともできます。

(後日追記)最終形態

上の状態でも思う通りに動いたのですが、拡張性を考えると、チェックボックスが増えるたびに変数が増えるのはあまりよろしくない、ということが分かりました。 そのため、複数のチェックボックスについて同様に動作させるべく、HTMLのタグに、 data- というカスタム属性を追加し、@json は使わない方向で書き換えました。

HTML
<input type="checkbox" name="chbox" data-checked={{ old('chbox', $checked) }}>
JS
// チェックボックスの表示
$('input[type="checkbox"]').each(function() {
    var checkbox = $(this);
    var checked = $(this).data('checked');
        if ( checked  == 'on' ) {
            checkbox.prop('checked', true);
        } else {
            checkbox.prop('checked', false);
        }
    }
});
  • data-checked には、oldがあればold, なければデフォルトをサーバから受け取った値が入ってくれます。

  • jQuery側の.data() メソッドで、カスタムで作ったdata-checked という属性の値を受け取っています。

  • .each にして、$(this) ごとにチェックを反映させるようにすることで、これからいくつチェックボックスが増えても data-checked 属性をつければ自動的に同じ動きをしてくれるようになりました!

以上、チェックボックスの操作結構めんどいけど、javascript書けばなんとかなったよ、という話でした。