Help us understand the problem. What is going on with this article?

ライブラリを使わずに、複数のチェックボックスにバリデーション制御をかけたい

html5から利用可能なプロパティのrequired、これは非常に便利で、フォームに対して入力漏れの確認を行ってくれます。そして、テキストボックス、プルダウンメニュー、ラジオボタン、テキストエリアでは、それをチェックするのに非常に役立つのですが、一つだけ厄介な部品があります。

チェックボックスの困った仕様

それがチェックボックスで、個々の部品に対してでしかバリデーションチェックしてくれないのです(そもそも、チェックボックスは名の通り、個々の項目に対してチェックを行うための部品なので、グループ化して使用するということを想定して設計されていません)。

それを踏まえて、下のhtmlを参照してください。

<form id="form" method="post" action="hogehoge.php">
    <p>Q1:球技に関心がありますか?</p>
    <label>ある</label><input type="radio" class="q1" name="q1" value="a1_1" required>
    <label>あまりない</label><input type="radio" class="q1" name="q1" value="a1_2" >

    <p>Q2:観戦が好きな球技は何ですか?(最低一つ以上、いくつでも答えてください)</p>
    <label>野球</label><input type="checkbox" class="q2" name="q2[]" value="a2_1" required>
    <label>サッカー</label><input type="checkbox" class="q2" name="q2[]" value="a2_2" >
    <label>バスケットボール</label><input type="checkbox" class="q2" name="q2[]" value="a2_3" >
    <label>バレーボール</label><input type="checkbox" class="q2" name="q2[]" value="a2_4" >
    <label>テニス</label><input type="checkbox" class="q2" name="q2[]" value="a2_5" >
    <label>ラグビー</label><input type="checkbox" class="q2" name="q2[]" value="a2_6" >
    <label>アメフト</label><input type="checkbox" class="q2" name="q2[]" value="a2_7" >
    <label>卓球</label><input type="checkbox" class="q2" name="q2[]" value="a2_8" >
    <button type="submit" id="bt_submit" name="bt_submit" value="submit">送信する</button>
</form>

このようなアンケートフォームがあります。これで何も選択せずに送信ボタンを押すとどうなるでしょうか?

まず、ラジオボタンにバリデーションチェックがかかりますが、ラジオボタンはグループ全体(name属性の値がグループとなっています)に対してバリデーションチェックが行われます。そこで、ブラウザからのエラーメッセージ通りに、任意のチェックを入れれば、ラジオボタンが要件を満たしたので、次はチェックボックスのバリデーションチェックを行ってくれます。

しかし、チェックボックスはグループ全体ではなく、前述したとおり個々の値に対してでしかバリデーションチェックが行われないので、上記サンプルのような記述方法だと、ほかの選択肢の有無はともかく、野球にチェックを入れない限り、「オプションを選択してください」という文言が出続けることになります。

そのため、validate.jsのような外部ライブラリに依存することになるのですが、どうしても外部のライブラリが動かない、または使えないという場合、どのように手動で制御するかというのが本題です。

チェックボックスを制御する

では、チェックボックス全部にrequired属性を付与してみます。

HTML
    <label>野球</label><input type="checkbox" class="q2" name="q2[]" value="a2_1" required>
    <label>サッカー</label><input type="checkbox" class="q2" name="q2[]" value="a2_2" required>
    <label>バスケットボール</label><input type="checkbox" class="q2" name="q2[]" value="a2_3" required>
    <label>バレーボール</label><input type="checkbox" class="q2" name="q2[]" value="a2_4" required>
    <label>テニス</label><input type="checkbox" class="q2" name="q2[]" value="a2_5" required>
    <label>ラグビー</label><input type="checkbox" class="q2" name="q2[]" value="a2_6" required>
    <label>アメフト</label><input type="checkbox" class="q2" name="q2[]" value="a2_7" required>
    <label>卓球</label><input type="checkbox" class="q2" name="q2[]" value="a2_8" required>

このようにすると、全部のチェックボックスにrequiredプロパティが機能しているため、全部のチェックボックスを選択しないと送信できなくなります。

…ですが、ここで逆転の発想、もしもrequiredプロパティが付与されているチェックボックスがゼロならば、バリデーションチェックがかかることありません。当たり前ですが、要は送信時のバリデーションチェックまでに、requiredプロパティを動的に0or100で制御すればいいわけです。

そのためにはJavaScript(ここではjQueryを使います。…ライブラリを使わずとか言ってますが、借り物を使わずにという意味で受け取ってください。なお、最後にJavaScriptだけで記述しています)のprop()メソッドを使います。そして、グループ全体に対してrequiredを付け外しすればいいわけです。

条件

  • グループ全体に対し、最低一つのチェックボックスにチェックが入っている(checked属性が付与されている)場合は、グループ全体のチェックボックスのrequired属性を解除する。
  • 逆に、チェックボックスにチェックが入っていない場合は、グループ全体のチェックボックスにrequired属性を付与する。

この2つのルールにしたがえば、簡単に制御できます。

javascript
   let checkedsum; //チェックが入っている個数
   $('.q2').on("click",function(){
      checkedsum = $('.q2:checked').length; //チェックが入っているチェックボックスの取得
      if( checkedsum > 0 ){
           $('.q2').prop("required",false); //required属性の解除
      }else{
           $('.q2').prop("required",true); //required属性の付与
      }
   });

こうすれば、チェックボックスでも、「最低一つ選択されているか確認する」という、本来想定していたバリデーション制御が簡単にできます。

応用編:バリデーションをカスタマイズしたい場合

もしも、ブラウザ依存のメッセージが利用者に分かりづらいので、自由に変えたいというとき、つまりバリデーションのメッセージをカスタマイズしたい場合は、javascriptのaddEventListenerを使います。そして、以下のような記述を使って、制御します。参考サイトはこちら

javascript
    validate = function( element){
        element.addEventListener("invalid", function(e) {
            if(element.validity.valueMissing){
                focus = e.target.type;
                if(focus == "checkbox"){
                    mes = "いずれかを選択してください";
                }
                e.target.setCustomValidity( mes);
            } else {
                e.target.setCustomValidity("");
                e.preventDefault();
            }
        },true);
    }

ところが、このイベントを発火してくれるタイミングの調整が結構曲者です。もし、以下のように記述してしまうと、チェックボックスにチェックを入れる度にバリデーションチェックを行ってしまうので、非常に動きが煩わしくなるばかりか、肝心の送信ボタンをクリックしたときに、バリデーションのカスタマイズができませんので意味がありません。

javascript
       $('.q2').each(function(index,element){
           validate(element);         
    });

ならば、送信ボタンを押すタイミングでイベントを発火すればいいのではないと思い、このようにしてみると…

javascript
    $('#bt_submit').on("click",function(index,element){
           validate(element);         
    });

送信ボタンもフォーム部品のため、送信ボタンに対するバリデーションのカスタマイズしかできませんので、チェックボックスのバリデーションが行われません。

…ではどうすればいいのか?

フォームタグに対して、バリデーションをかけます。しかし、バリデーションチェックが行われるのはsubmitする前のようで、クリックイベントの実施に対し、発火するようにします。

javascript
    $('#form').on("click",function(){
            $('.q2').each(function(index,element){
                validate(element);
            })
    });

これで、フォーム部品がなにか触ったときにイベントが起きるので、チェックボックス、送信ボタンにかかわらず、クリックイベントが起きたときにバリデーションチェックが行われます。ただ、これだとまだ煩わしいので、以下のように制御します。

javascript
    let focustag; //フォーカスされたタグを格納
    $('#form').on("click",function(){
        focustag = $(':focus').attr("type"); //フォーカスされたタグを取得
        if(focustug == "submit"){
            $('.q2').each(function(index,element){
                validate(element);
            })
         }else{
             return;
         }
    });

このようにすれば、submitボタンが実行された場合のみ、イベントを発火させることができ、カスタマイズされたエラーメッセージを表示したりできるようになります。ですが、まだ大きな問題があります。

送信のタイミングがワンテンポずれる

これがかなり曲者で、一度空のメッセージが表示された後、フォームが実行されてしまう現象が発生します(理由はかなりややこしいので割愛しますが、どうやらこの現象のようです)。ところが、この空のメッセージが表示される現象、利用者には一度目の送信ボタンが効かないと受け止められてしまい、このままでは利用者にとっても煩わしくなってしまうので、これを制御します。

submitイベントを埋め込む

それならば、バリデーションチェックで問題が発生しなくなった場合のタイミングで送信イベントが起きるようにします。ところが、htmlだけだとどうしても限度があるようなので(どうやっても解決しませんでしたので、方法があったら教えてください)、スクリプト内にsubmitイベントを仕掛けます(前述した通り、submitイベントのタイミングだとバリデーションのカスタマイズができません)。

ただ、今のスクリプトのままだと、イベント記述に適したタイミングがありません。

required属性に目をつける

そこで、新たに判定文を作ります。そのタイミングの鍵を握るのがrequiredプロパティで、この個数がゼロになったタイミングでトリガーを発火させます。

javascript
    let focustag; //フォーカスされたタグを格納
    let requiredsum; //requiredプロパティを持ったフォーム数を格納
    $('#form').on("click",function(){
        focustag = $(':focus').attr("type"); //フォーカスされたタグを取得
        if(focustag == "submit"){
            requiredsum = $('.q2:required').length; //requiredプロパティを持ったフォームを取得
            if(requiredsum > 0){
                $('.q2').each(function(index,element){
                    validate(element); //バリデーションチェック
                })
            //バリデーションチェックが不要になったので、submitを実行する
            }else{
                var hd = $("<input>").attr({type:"hidden",name:"bt_submit"}).val("submit");
                hd.appendTo($('#form'));
                $('#form').attr({method:"post",action:"hogehoge.php"});
                $('#form').submit();
            }
         }else{
             return;
         }
    });

これで、バリデーションをカスタマイズしつつ、requiredプロパティがゼロになった状態でsubmitボタンを押したら転送されるようになりました。

補足1:チェックボックスのグループが複数の場合はどうしたらいい?

チェックボックスのグループが複数の場合は、このようにクラス名を取得するなどして制御します。

javascript
   let selclass; //選択されたクラス名
   let dir; //イベントが行われたクラス
   let checkedsum; //チェックが入っている個数
   $('checkbox').each(function(){
       $(this).on("click",function(){
          selclass = $(this).attr('class');
          dir = $("."+selclass[0]);
          checkedsum = dir.filter(':checked').length; //チェックが入っているチェックボックスの取得
          if( checkedsum > 0 ){
              dir.prop("required",false); //required属性の解除
          }else{
              dir.prop("required",true); //required属性の付与
          }
       })
   });

補足2:バリデーションの文言を変えたい

e.target.xxxxを使えば、タグは属性、クラス名、名前などを取得できるので、そこでメッセージを分岐すればいいでしょう。たとえば、ラジオボタンとチェックボックスでメッセージを振り分けたい場合はこのように記述すれば大丈夫です。

javascript
 focus = e.target.type;
 if(focus == "radio"){
    mes = "いずれかを選択してください";
 }else if(focus == "checkbox"){
    mes = "最低一つ選択してください"
 }
 e.target.setCustomValidity( mes);

応用2:部品が複合した入力フォームの場合

この方法を使えば、色々なフォームが複合している場合でも応用(たとえば、自由記入欄(テキストエリア)付きのチェックボックスの場合など)できます。

PHP
<p>Q5:球技で遊ばない理由は何ですか?(最低一つ以上、いくつでも答えてください)</p>
<label>運動・球技が苦手だから</label><input type="checkbox" class="q5" name="q5[]" value="a5_1" required>
<label>仲間が集まらないから</label><input type="checkbox" class="q5" name="q5[]" value="a5_2" 
 required>
<label>遊ぶ場所がないから</label><input type="checkbox" class="q5" name="q5[]" value="a5_3" required>
<label>お金がかかるから</label><input type="checkbox" class="q5" name="q5[]" value="a5_4" 
 required>
<label>危険だから</label><input type="checkbox" class="q5" name="q5[]" value="a5_5" required>
<label>その他(自由に意見をお書きください)</label>
<textarea class="q5" name="q5[]" value="a5_6" required><!-- 自由記入欄 --></textarea>

応用3:JavaScriptだけで制御

行数は多くなりますが、javascriptだけでも制御可能です。もっとシンプルに書けるかも知れません。

js
<script>
   let rdClass = f.q1;
   let selClass = document.getElementsByClassName('q2');
   let focustag; //フォーカスされたタグを格納
   let focustype; //フォーカスされたフォーム部品の種類
   let requiredsum; //requiredプロパティを持ったフォーム数を格納
   let checkflg = 0; //checkedプロパティのフラグ
   let reqflg = 0; //requiredプロパティの設定フラグ
   let t_input;
   f.addEventListener("click",function(){
        focustag = document.activeElement.type;
        if( focustag == "submit"){
            if(checkflg == 0 || reqflg == 0){
                for( elem of rdClass){ validate(elem)};
                for( elem of selClass){ validate(elem)};
            }else{
                t_input = document.createElement('input');
                t_input.setAttribute("type","hidden");
                t_input.setAttribute("name","bt_submit");
                t_input.setAttribute("value","submit");
                f.setAttribute("method","post");
                f.setAttribute("action","test_js.php");
                f.appendChild(t_input);
                f.submit();
            }
        }else if( focustag == "radio"){
            checkRadio();
        }else if( focustag == "checkbox"){
            setRequired();
        }
   })

    validate = function( element){
        element.addEventListener("invalid", function(e) {
            if(element.validity.valueMissing){
                focustype = e.target.type;
                if(focustype == "checkbox"){
                    mes = "最低一つ選択してください";
                }else if(focustype == "radio"){
                    mes = "いずれかを選択してください";
                }
                e.target.setCustomValidity( mes);
            } else {
                e.target.setCustomValidity("");
                e.preventDefault();
            }
        },true);
    } 
   //ラジオボタンの制御
   checkRadio= function(){
        checkflg = 0;
        for( prop of rdClass ){
            if(prop.checked == true){
                checkflg = 1;
                break;
            }
        }
   }
   //requiredプロパティの制御
   setRequired = function(){
        reqflg = 0;
        //チェックボックスの制御
        for( prop of selClass){
            if(prop.checked == true){
                reqflg = 1;
                break;
            }
        }
        if(reqflg == 1){
            for (prop of selClass){ prop.required = false};
        }else{
            for (prop of selClass){ prop.required = true};
        }
   }
</script>
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away