【Javascript】フォームバリデーションを実装してみた

#はじめに
割り勘額を計算してくれる簡易Webアプリを作成中に、Javascriptでフォームバリデーションを実装してみたので学習メモとして残しておく。CSSでの細かいスタイリングは記載しない。
当方、初学者のため記事の内容に間違いや改善点などございましたら、ぜひコメントでご指摘ください。

#要件
「追加」「削除」(1～8人)が可能なフォームにて、以下の要件を満たすバリデーションを実装したい。
※フォームの追加、削除方法に関しては以下の記事に記載しています。

[templateタグを使ってフォームを複製]
[templateタグを使ってフォームを複製]:[https://qiita.com/cookiesand1023/items/d7e25d51e3a9143879f6]
[【Javascript】remove()で要素を削除する]
[【Javascript】remove()で要素を削除する]:[https://qiita.com/cookiesand1023/items/3cd57fd51f85ff920990]



▼個々のフォームのルール
・名前:12文字以下。
・年齢:半角英数字のみ。
・性別:男性、女性どちらか一方が選択されている。

▼全体のルール
・追加した分も含めて、すべてのフォームが入力されている。
・「計算」ボタンをクリックするとバリデーションの関数を実行する。
・バリデーションエラー時にそれぞれのフォームに関するエラーメッセージを出力。
また入力内容に間違いがあるフォームのスタイルを変更する。


###フォーム入力前
![スクリーンショット (17).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388451/70b70b6a-0cd9-9093-5e70-7e00539747e5.png)

###入力内容エラー時
![スクリーンショット (18).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388451/1360994b-772d-6fa9-3317-fa3115caefdd.png)



#フォームのHTML
```HTML:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
    <script type="text/javascript" src="addForm.js"></script>
    <script type="text/javascript" src="deleteForm.js"></script>
    <script type="text/javascript" src="validation.js"></script>
</head>
<body>
    <div class="input-form" id="input-form">
        <!--テンプレートを作成-->
        <!--name属性にはフォーム追加時にインデント番号を付与する-->
        <template id="form-template">
            <div class="member" id="member">
                <input type="text" size="20" name="" placeholder="名前">
                <input type="text" size="3" name="" placeholder="年齢">
                <label>
                    <input type="radio" name="" value="男性" id="male">男性
                </label>
                <label>
                    <input type="radio" name="" value="女性" id="female">女性
                </label>
            </div>
        </template>
    </div>
    <!--バリデーションエラー時に出力するメッセージを用意しておく(display: none)-->
    <div class="err-message" id="err-message">
        <p id="nameerr-msg" style="display: none;">※名前を正しく入力してください(1～20文字)</p>
        <p id="ageerr-msg" style="display: none;">※年齢を正しく入力してください(半角英数字)</p>
        <p id="sexerr-msg" style="display: none;">※性別を選択して下さい</p>
    </div>
    <!--「追加」ボタンをクリックしたらJavascriptファイル内の関数addForm()を実行する-->
    <div class="bt_addForm">
        <input type="button" value="追加" onclick="addForm()">
    </div>
    <!--「削除」ボタンをクリックしたらJavascriptファイル内の関数deleteForm()を実行する-->
    <div class="bt_deleteForm">
        <input type="button" value="削除" onclick="deleteForm()">
    </div>
    <!--「計算」ボタンを用意-->
    <div class="bt_culculate" id="bt_culculate">
        <input type="button" value="計算">
    </div>    

</body>
</html>
```


##フォーム追加の関数
以下のスクリプトにて、HTML内のテンプレートから実際のフォームを追加する際にインデント番号を付与している。(1人目だと名前フォームのname属性は"name-1"、年齢フォームのname属性は"age-1"のような感じ、、)

```Javascript:addForm.js
{

function addForm() {
    let child_length = document.querySelectorAll("div.member").length;
    let i = child_length+1;

    // 8人以上なら処理を終了する
    if (i > 8) {
        return;

    } else {
        // HTMLからtemplate要素を取得
        const template = document.getElementById("form-template");

        // templateの内容を複製
        const new_form = template.content.cloneNode(true);

        // 子要素を指定しname属性の値を変更
        const new_div = new_form.children[0];
        new_div.id = 'member-'+i;

        const new_form_name = new_form.children[0].children[0];
        new_form_name.name = 'name-'+i;

        const new_form_age = new_form.children[0].children[1];
        new_form_age.name = 'age-'+i;

        const radio_male = new_form.children[0].children[2].children[0];
        radio_male.name = 'sex-'+i;

        const radio_female = new_form.children[0].children[3].children[0];
        radio_female.name = 'sex-'+i;

        //親要素を取得し 複製した要素を追加
        const parent = document.getElementById("input-form");
        parent.appendChild(new_form);

    }
}

//ページ読み込み時に関数addForm()を1回実行
window.addEventListener('DOMContentLoaded', addForm);
    
}


```


#フォームバリデーション実装(1人分)
いきなり全体のバリデーション処理を実装するのは自分には難しかったので、まず1人分のフォームに対してバリデーション処理を実装してみる。1人分のフォームバリデーションに必要な処理は以下の三つ。

・入力された名前が1～20文字でなければエラーメッセージ出力&スタイル変更
・入力された年齢が半角英数字1～2桁でなければ、エラーメッセージ出力&スタイル変更
・ラジオボタンが選択されていなければエラーメッセージ出力

```Javascript:validation.js
    // ボタン取得
    const button = document.getElementById("bt_culculate");

    //エラー、メッセージ取得
    const nameErrMsg = document.getElementById("nameerr-msg");
    const ageErrMsg = document.getElementById("ageerr-msg");
    const sexErrMsg = document.getElementById("sexerr-msg");

    // ボタン押下で関数inputCheck実行
    button.addEventListener(`click`, inputCheck, false);

    // 関数宣言
    function inputCheck() {
            
        //エラーフラグを設定
        let name_err_flag = true;
        let age_err_flag = true;
        let sex_err_flag = true
        

        //名前フォームとその文字数を取得
        const input_name = document.getElementsByName("name-1");
        const name_count = input_name[0].value.length;

        //1～20文字でなければエラーフラグをたてる
        if (name_count === 0 || name_count > 20) {
            name_err_flag = false;
            //エラー時スタイル変更
            input_name[0].style.backgroundColor = `#ffb6c1`;
        
        } else {
            input_name[0].style.backgroundColor = ``;
        }


        // 年齢フォームとその値を取得
        const input_age = document.getElementsByName("age-1");
        const age_value = input_age[0].value;

        //半角英数字1～2桁でなければエラーフラグをたてる
        if (!age_value.match(/^[0-9]{1,2}$/)) {
            age_err_flag = false;
            //エラー時スタイル変更
            input_age[0].style.backgroundColor = `#ffb6c1`;

        } else {
            input_age[0].style.backgroundColor = ``;
        }


        //性別とラジオボタンの数を取得
        const input_radio = document.getElementsByName("sex-1")
        const radio_count = input_radio.length;

        //ラジオボタン選択チェック用のフラグを設定する
        let radio_check_flag = false;

        //ラジオボタン選択チェック
        for (let i = 0; i < radio_count; i++) {
            
            //選択されていた場合、フラグの値をtrueに変更
            if (input_radio[i].checked) {
                radio_check_flag = true;
            }
        }

        //ラジオボタンのエラーフラグがfalseだった場合、バリデーションのエラーフラグをたてる
        if (radio_check_flag === false) {
            sex_err_flag = false;
        }


        //バリデーションのエラーフラグがfalseであればエラーメッセージを表示する
        //trueの場合はエラーメッセージを隠す
        if (name_err_flag === false) {
            nameErrMsg.style.display = `block`;

        } else {
            nameErrMsg.style.display = `none`;
        }

        if (age_err_flag === false) {
            ageErrMsg.style.display = `block`;

        } else {
            ageErrMsg.style.display = `none`;
        }

        if (sex_err_flag === false) {
            sexErrMsg.style.display = `block`;

        } else {
            sexErrMsg.style.display = `none`;
        }

    }
```

最初にエラーフラグをtrueで設定しておいて、入力エラーがあった場合フラグをfalseに変更する。
関数の最後でエラーフラグのtrue/falseを判定し、trueならエラーメッセージを隠し、falseなら表示する。
この方法ならバリデーションを実行する際に、エラーメッセージが表示されている状態or隠されている状態を判別する必要がない。

####※addEventListener使用時の注意点
最初、button要素をgetElementbyClassNameで取得しようと試したが、addEventListenerの部分でエラーが出た。どうやらgetElementsbyClassNameで取得できるのはHTMLCollectionというオブジェクト(複数ノード)で、単一ノードのみ指定できるaddEventListnerでは取得することができないらしい。


#フォームバリデーション実装(2人以上)
では、上で実装したフォームバリデーションをfor文でループさせて、フォームが増えた後も正しく動作するように変更する。

```Javascript:validation.js
window.onload = function() {
        
    // ボタン取得
    const button = document.getElementById("bt_culculate");

    //エラー、メッセージ取得
    const nameErrMsg = document.getElementById("nameerr-msg");
    const ageErrMsg = document.getElementById("ageerr-msg");
    const sexErrMsg = document.getElementById("sexerr-msg");

    // ボタン押下で関数inputCheck実行
    button.addEventListener(`click`, inputCheck, false);

    // 関数宣言
    function inputCheck() {
    
        // メンバーの数を取得
        const form_length = document.querySelectorAll("div.member").length;
        
        //エラーフラグを設定
        let name_err_flag = true;
        let age_err_flag = true;
        let sex_err_flag = true;

            //メンバーの数だけバリデーション処理を実行
            for (i=1; i<=form_length; i++) {

                //名前とその文字数を取得
                const input_name = document.getElementsByName("name-"+i);
                const name_count = input_name[0].value.length;

                // 年齢とその文字数を取得
                const input_age = document.getElementsByName("age-"+i);
                const age_value = input_age[0].value;

                //1～20文字でなければエラーフラグをたてる
                if (name_count === 0 || name_count > 20) {
                    name_err_flag = false;
                    //エラー時スタイル変更
                    input_name[0].style.backgroundColor = `#ffb6c1`;
                
                } else {
                    input_name[0].style.backgroundColor = ``;
                } 

                //半角英数字1～2桁でなければエラーフラグをたてる
                if (!age_value.match(/^[0-9]{1,2}$/)) {
                    age_err_flag = false;
                    //エラー時スタイル変更
                    input_age[0].style.backgroundColor = `#ffb6c1`;

                } else {
                    input_age[0].style.backgroundColor = ``;
                }


                //性別とラジオボタンの数を取得
                const input_radio = document.getElementsByName("sex-"+i)
                const radio_count = input_radio.length;

                //ラジオボタン選択チェック用のフラグを設定する
                let radio_check_flag = false;

                //ラジオボタン選択チェック
                for (let j = 0; j < radio_count; j++) {
                    
                    if (input_radio[j].checked) {
                        radio_check_flag = true;
                    }
                }
                //未チェックならエラーフラグをたてる
                if (radio_check_flag === false) {
                    sex_err_flag = false;
                }

            }

        //フラグがfalseであればエラーメッセージを表示する
        if (name_err_flag === false) {
            nameErrMsg.style.display = `block`;

        } else {
            nameErrMsg.style.display = `none`;
        }

        if (age_err_flag === false) {
            ageErrMsg.style.display = `block`;

        } else {
            ageErrMsg.style.display = `none`;
        }

        if (sex_err_flag === false) {
            sexErrMsg.style.display = `block`;

        } else {
            sexErrMsg.style.display = `none`;
        }
    }
}
```

###window.onload
window.onloadを冒頭に書かなかった場合、addEventListener部分でエラーが出た。これはDOMツリーを読み込む前にjs側でボタン要素を取得しようとしたため、変数buttonがnullとなりエラーが生じたみたい。なのでwindow.onloadで、このvalidation.js自体をDOM読み込み完了後に実行するようにした。


#まとめ
今回フォームバリデーションを初めて実装したが、複数フォーム×複数人という少し複雑なパターンだったので学べたことも多かったように思う。エラーメッセージの表示/非表示部分などは、表示状態を判定せずに力技で毎回変更するように設定しているので、もう少し処理が速くできそうな方法も探してみたい。