Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
35
Help us understand the problem. What is going on with this article?
@BRSF

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

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属性の付与
      }
   });

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

ですが、各種ブラウザでは用意されていたメッセージは、たとえばChromeの場合「次に進むには、このチェックボックスをオンにしてください」、IEの場合「このチェックボックスをオンにしてください」など本来のチェックボックスを意識した内容となっており、このままでは利用者に誤解を招く可能性があります。

ですので、次章ではどうやってバリデーションをカスタマイズするか解説していきます。

応用編:バリデーションをカスタマイズする

バリデーションのメッセージをカスタマイズしたい場合は、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ボタンを押したら転送されるようになりました。

ですが、これだとformタグをスクリプトで動的に生成しているので、記述が面倒ですしリスクもあります。そこで、もっといい方法がないかをずっと探していましたら、決定的な方法が見つかりました。

keyupイベントで制御する

先程参考にしたこのサイトのようにkeyupイベントで制御すれば、フォーム転送のタイミング問題もクリアされ、スクリプトへのタグ記述が不要となりました。ですが、ここではあくまでテキストボックスやプルダウンの場合で、チェックボックスについては触れられていません。

そこでチェックボックスでも応用できないか調べてみましたが、valueMissngの挙動がかなり厄介で、チェックボックスの場合は値が選択された場合でもcustomValidityを空白化しないといつまでもチェックに引っかかってしまいます。そこで、前述したようにチェックボックスのrequiredプロパティを0or100で加除しながらチェックボックスのバリデーションが必要なタイミングで操作を振り分け、バリデーションが不要になったら空白化させるとすべての処理がうまくいきました。

js
    //チェックボックスのバリデーション制御
   let requiredsum;
   let focus;
   let mes;
   custom = function(element){
       if(element.validity.valueMissing){
           //mes = "この項目は必須です";
           //element.setCustomValidity( mes);
       }else if(element.validity.patternMismatch){
           //mes = "不正な値が入力されています。入力例を確認してください";
           //element.setCustomValidity(mes);
       }else{
           element.setCustomValidity(""); //チェックボックスのバリデーションを空にする
       }
   }
   validate = function( element){
       requiredsum = $('.q2:required').length;
       if(requiredsum > 0 ){
           mes = "いずれかを選択してください";
           element.setCustomValidity( mes);
       }else{
           custom(element); //バリデーションメッセージのカスタマイズ
       }
       element.addEventListener("keyup", function(e) {
           custom(element); //バリデーションメッセージのカスタマイズ
           e.preventDefault();
       });
   }

   $('#form').on("click",function(){
       $('.q2').each(function(index,element){
            validate(element); //バリデーションチェック
       })
  })


補足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 他のフォームと複合した場合

もし、複合したフォームの中に、テキストボックス、プルダウンメニュー、テキストエリアが存在している場合はrequiredプロパティの個数を調べても無意味(requiredプロパティはバリデーションに関係なく、数は増減しない)なので、いつまで経っても転送することができません。そこで、色々と工夫が必要になります。

element.typeでフォームの属性を取得できるので、checkbox属性かそれ以外を振り分け、チェックボックスの場合はバリデーションの文言を削除、それ以外は普通にバリデーションチェックを通すと、以下のように別のフォームと複合した場合でも対応できます。

js
                //チェックボックスのバリデーション制御
                let requiredsum;
                let focus;
                let mes;
                custom = function(element){
                    if(element.validity.valueMissing){
                        mes = "この項目は必須です";
                        element.setCustomValidity( mes);
                    }else if(element.validity.patternMismatch){
                        mes = "不正な値が入力されています。入力例を確認してください";
                        element.setCustomValidity(mes);
                    }else{
                        element.setCustomValidity(""); 
                    }
                }
                validate = function( element){
                    requiredsum = $('.q2:required').length;
                    focus = element.type;
                    if(requiredsum > 0 && focus == "checkbox"){
                        mes = "いずれかを選択してください";
                        element.setCustomValidity( mes);
                    }else{
                        custom(element); //バリデーションメッセージのカスタマイズ
                    }
                    element.addEventListener("keyup", function(e) {
                        custom(element); //バリデーションメッセージのカスタマイズ
                        e.preventDefault();
                    });
                }

                $('#form').on("click",function(){
                    $('input,select,textarea').each(function(index,element){
                         validate(element); //バリデーションチェック
                    })
               })

Javascriptだけで制御

行数は多くなりますが、JavaScriptだけでも制御可能です。

before

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>

after

keyupイベントの利用で、jQueryで書き直したものを参考に、JavaScriptでも同様に書き直してみました(まだ、jQueryに依存してた場所があったので、それも書き直してます)。

js
   let selClass1 = document.getElementsByClassName('q1');
   let selClass2 = document.getElementsByClassName('q2');
   let focustype; //フォーカスされたフォーム部品の種類
   let requiredsum; //requiredプロパティを持ったフォーム数を格納
   let checkflg = 0; //checkedプロパティのフラグ
   let reqflg = 0; //requiredプロパティの設定フラグ
   let mes;

   custom = function(element){
       if(element.validity.valueMissing){
           mes = "この項目は必須です";
           element.setCustomValidity( mes);
       }else if(element.validity.patternMismatch){
           mes = "不正な値が入力されています。入力例を確認してください";
           element.setCustomValidity(mes);
       }else{
           element.setCustomValidity(""); //チェックボックスのバリデーションを空にする
       }
   }
   validate = function( element){
       requiredsum = checkRequired(element.className);
       focustype = element.type;
       if(requiredsum > 0 && focustype == "checkbox"){
           mes = "いずれかを選択してください";
           element.setCustomValidity( mes);
       }else{
           custom(element); //バリデーションメッセージのカスタマイズ
       }
       element.addEventListener("keyup", function(e) {
           custom(element); //バリデーションメッセージのカスタマイズ
           e.preventDefault();
       });
   }

   //requiredプロパティの確認
   checkRequired = function(selclass){
       let flg = 0;
       len = document.getElementsByClassName(selclass).length;
       for(var i = 0 ; i < len  ; i++ ){
           if(document.getElementsbyByClassName(selclass)[i].required){
               flg = 1;
               break;
           } 
       }
       return flg;
   }

   document.f.addEventListener("click",function(){
        document.querySelectorAll('input').forEach(function(element){
            validate(element); //バリデーションチェック
        })
   })

    document.f.addEventListener("change",function(){
            focustype = document.activeElement.type;
            if(focustype == "checkbox"){
                setRequired(elem.target.className); //チェックボックスの値を制御
            }
    })


   //チェックボックスにおけるrequiredプロパティの制御
   function setRequired(selClass){
        reqflg = 0;
        let obj = document.getElementByClassName(selClass);
        //チェックボックスの制御
        for( prop of obj){
            if(prop.checked == true){
                reqflg = 1;
                break;
            }
        }
        if(reqflg == 1){
            for (prop of obj){ prop.required = false}; //一つでもチェックされている場合
        }else{
            for (prop of obj){ prop.required = true}; //何もチェックされていない場合
        }
   }

複数のチェックボックスの値をリアルタイムで取得する部分を調べるのに苦労しましたが、下記のようにフォームタグに対して、イベントリスナーで発火させるといいようです。こうすることで、チェックボックスの有無によってrequiredプロパティを付け外しします。また、クラス名は随時、アクティブとなっているものを取得するようにすると、複数の場合でも対応させることができます。

また、ラジオボタンも応用3には登場しなかったですが、上記の方法で制御できました(結局、動的にsetCustomValidityの値を空白化しないといけないフォーム部品はチェックボックスだけのようです)。

それからオブジェクトの値を逐一取得する場合はfor(obj of objects){ …}式を用いれば、jQueryとそこまで記述文字数に差がなくなるので、非常にすっきりした記述となります。

35
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  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
BRSF
職業、PG・SE・DBエンジニア。オープン環境のwebプログラムをメインにシステム構築担当。使用言語はPHP(cakePHP、Laravel含)jQuery、JavaScript、ExcelVBA、Perl、Ruby、Python、Vue、React、Angularなど。 teratailでも別アカウントにて参加。PHPでゴールドタグとってます。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
35
Help us understand the problem. What is going on with this article?