HTMLフォームにjQuery Validation Engineでバリデーションを走らせたり連打対策してみる

本投稿は[株式会社ピーアールオー（あったらいいな！を作ります） Advent Calendar 2021](https://qiita.com/advent-calendar/2021/pro-japan1)の**12日目の予定でした**
昨年に引き続き「でした。」になっちゃいました。遅延は昨年より更にひどい。
絶賛年末進行中&緊急対応してたんで許してくださいwww

ここ数年は、ほとんどコーディングをせずに過ごしているんですが、久々にHTMLフォームを作ったので覚書として記事を書きます。

#どちらが「吸収するか」問題
フォームを設置する場合、デザイナーとプログラマーで「どこまでを担当する」問題というのがありまして、今までの経験では「**エラーがどこに出るか、内容を全出ししてコーディングして**」までがほとんど。
バリデーションを走らせたり、状態を変化させる仕組みまでは考えない。

とはいえ。

仕事をしていると、簡易のバリデーションを走らせたり、状態を変化させたりする仕組みを作らなくてはいけないこともある。

#フォームを作る
まあ、普通にコーディングします。IEは当然ガン無視[^ガン無視]です。
本当はアクセシビリティを考慮した内容にもしたかったけど、時間がないので普通にコーディング。
今回は裏側なしなので、「送信」ボタンを押下してもエラー表示が出てくるだけですw

ターゲットによりますが、モバイルファーストで画面を作るのがデフォなので、今回もスマホで使うことを前提にコーディングします。
指でタップしやすいよう、セレクトボックスやラジオボタンもCSSを使って装飾♪装飾♪

HTML自体は何の変哲もない姿をしていますが、CSSは嫌な感じになりますw

[^ガン無視]: やっと解放されるね⭐️やっほーい！参考：[Microsoft 社 Internet Explorer のサポート終了について：IPA 独立行政法人 情報処理推進機構](https://www.ipa.go.jp/security/announce/ie_eos.html)

```html:HTML
<dl class="contactForm">
  <dt class="contactForm_term">
    <label for="approach">
      連絡方法
      <span class="contactForm_term_requiredMark">
      必須
      </span>
   </label>
  </dt>
  <dd class="contactForm_desc contactForm_desc-radio">
    <input type="radio" name="otherContact" value="メールアドレス" id="approach01">
     <label for="approach01">メールアドレス</label>

       <div class="contactForm_desc_comments">
         <input type="radio" name="otherContact" value="電話番号" id="approach02">
         <label for="approach02">電話番号</label>
         <div class="contactForm_desc_comments_input">
           <input type="tel" name="otherContactComent">
         </div>
       </div><!-- /.contactForm_desc_comments -->

       <div class="contactForm_desc_comments">
         <input type="radio" name="otherContact" value="その他" id="approach03">
         <label for="approach03">その他</label>
         <div class="contactForm_desc_comments_input">
           <input type="text" name="otherContactComent">
         </div>
      </div><!-- /.contactForm_desc_comments -->
    </dd>
</dl>
```
デフォルトのラジオボタンを消して、```label```要素に```::before```やら```::after ```やらでラジオボタンぽいものを作ります。
HTMLやCSSに強いプログラマーもいますが、これを探したり・書いたり・改善したりなんて無理だよねえ。デザイナーでも「無茶な！」って思うことがありますw

```css:CSS
.contactForm_desc {
 position: relative;
 padding: .5rem;
 margin-bottom: .5rem;
 background: #edf0f5;
}

.contactForm_desc-radio [type="radio"] {
  visibility:hidden;
  position: absolute;
  top: 1rem;
  left: .5rem;
}

.contactForm_desc-radio label {
  display:block;
  position: relative;
  padding: 1rem 1rem 1rem 2.8rem;
  margin-top: .5rem;
  text-align: left;
  cursor: pointer;
}

.contactForm_desc-radio label::before,
.contactForm_desc-radio label::after {
  content: "";
  display: block; 
  border-radius: 50%;
  position: absolute;
  transform: translateY(-50%);
  top: 50%;
}

.contactForm_desc-radio label::before {
  width: 1.5rem;
  height: 1.5rem;
  left: .5rem;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 50%;
}

.contactForm_desc-radio label::after {
  width: 0;
  height: 0;
  background: #80bced;
  border-radius: 50%;
  opacity: 0;
}

.contactForm_desc-radio [type="radio"]:checked + label::after {
  width: 1rem;
  height: 1rem;
  left: .8rem;
  opacity: 1;
  -webkit-transition: all .12s, border-color .08s;
  transition: all .12s, border-color .08s;
}

.contactForm_desc-radio [type="radio"]:checked + label {
  background: #80bced;
  color: #fff;
  font-weight: bold;
}

.contactForm_desc_comments_input {
  padding: calc(0.65rem - 2px);
}

.contactForm_desc-radio [type="radio"]:checked + label + .contactForm_desc_comments_input {
  background: #80bced;
}
```

#バリデーションはjQueryのプラグインで
一部でオワコン言われているけど私は使うぞ。
[jQuery Validation Engine](https://github.com/posabsolute/jQuery-Validation-Engine)

言語ファイルも用意されているので、他言語化されたサイトでも利用可能。
今回のような、ボタンの状態に合わせてテキストフォームにエラーを出すとかもできる。

```html:HTML
<head>
  <!-- CSS -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jQuery-Validation-Engine/2.6.4/validationEngine.jquery.min.css">

  <!-- Javascript -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/posabsolute/jQuery-Validation-Engine@3.1.0/js/jquery.validationEngine.min.js"></script>
  <script src="https://cdn.jsdelivr.net/gh/posabsolute/jQuery-Validation-Engine@3.1.0/js/languages/jquery.validationEngine-ja.js"></script>
</head>
```

必要なjsとcssを読み込んで、最低限こう書けば良い。

```js:Javascript
$(document).ready(function(){
    $("#contact").validationEngine();
   });
```

```#contact```は``` form```タグについているID。フォームががひとつだけなら```form```タグを直接入れてもいいかもしれないけど、あまりお勧めしない。
後でフォームが複数になったら超面倒だからw

あとは必須項目にCLASSを追加する。

```html:HTML
<input type="radio" name="otherContact" value="メールアドレス" id="approach01" class="validate[required]">
<label for="approach01">メールアドレス</label>
```

CLASSの```validate[required]```はベーシックなエラーが表示されます。
オプションも色々あるので、詳しくは[jQuery Validation Engine](https://github.com/posabsolute/jQuery-Validation-Engine)を確認されたし。

いにしえのテーブルコーディング時代[^いにしえのテーブルコーディング時代]からHTMLを書いているので、ちょっとしたコーディングはまあまあできると思いますが、jQueryとそのプラグインを使えばメロっとこんな感じまではできる。
![簡単なHTMLフォーム](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44795/62e1fc5c-47a4-fd44-abeb-5e56bb6a61d3.jpeg)

## バリデーションのテキストを変更したい
「\*必須項目です」の頭の「\*」が気になるし、「選択してください」「名前を入れてください」に変えたいところもある。
[jQuery Validation Engine](https://github.com/posabsolute/jQuery-Validation-Engine)は痒いところに手が届くので、オプションを書いてみます。今回は外部jsに書く方法。
個別にテキストを変えたい場合は、IDを割り振って、それに対してテキストを変更します。

```js:Javascript
$("#contact").validationEngine('attach', {
    promptPosition : "topLeft: 0", // エラーの位置指定
    showArrowOnRadioAndCheckbox: true, // ラジオボタンやチェックボックスのエラーに吹き出しを付ける
    "custom_error_messages": { //エラーテキストオプション
      "required": {            //デフォルトのエラーテキスト
        "message": "必須項目です"
        },
        "#contactDetail": {
          "required": {
              "message": "選択してください"
          }
        },
      "#name": {
        "required": {
          "message": "名前を入れてください"
        }
      },
      "#approach01": {
        "required": {
          "message": "選択してください"
        }
      },
      "custom[email]": {
        "message": "正しいメールアドレスを入れてください"
      },
      "#agree": {
        "required": {
          "message": "確認してください"
        }
      }
    }
});
```
![エラーテキスト変更](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44795/03aaae62-9a27-ca52-bb2e-dd3ee5aaa0b4.jpeg)
1箇所だけとかなら```data-errormessage```を使う方法がいいかも。

```html:HTML
<input type="email" name="userMail" id="email" class="validate[required, custom[email]]" data-errormessage="メールアドレスが間違っていますよ">
```
## フォーム部品に装飾を施すとちょっと面倒になる
すんなり実装できた風ですが、実際は違います。
フォーム部品に装飾を施すと、プラグインがうまく動かないことがあります。

今回のプラグインの場合、```input```に対してエラーを表示するので、```input```を不用意に消すとエラーが表示されなくなります。

そのため、ちょっとした工夫が必要。

```css:CSS
.contactForm_desc {
   position: relative;
   padding: .5rem;
   margin-bottom: .5rem;
   background: #edf0f5;
}

.contactForm_desc-radio [type="radio"] {
  visibility: hidden;
  position: absolute;
  top: 1rem;
  left: .5rem;
}
```
装飾したいがために、本来のラジオボタンを```visibility: hidden;```で不可視にしています。
こういう場合は```display: none;```で消すことがほとんどなのですが、今回のjQueryプラグインでは、不用意に要素を消すと「エラーが表示されない」というドツボにハマります。

なぜなら、見た目はどちらも「消えてる」のですが、その**「消え方」に違い**があります。

### レイアウトを変更せず要素を不可視にする visibilityプロパティ

> visibility は CSS のプロパティで、文書のレイアウトを変更することなく要素を表示したり非表示にしたりします。このプロパティは \<table> の行や列を隠すこともできます。
>
> 要素を不可視にしてレイアウトから除去するには、 visibility を使用する代わりに display プロパティを none に設定してください。

引用：[visibility - CSS: カスケーディングスタイルシート | MDN](https://developer.mozilla.org/ja/docs/Web/CSS/visibility)

レイアウトから**除去**してしまう```display: none;```を使うと```input```が存在しないことになり、エラーが表示されなくなります。
それではまずいので、**不可視**になる```visibility: hidden;```を使います。
![プロパティによる違い](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44795/4ef3ae92-ea5e-6fce-48cf-16dbc4fbebbd.jpeg)
これで解決するかと思いきや…なかなかうまくいかない。
レイアウトから除去してしまう```display: none;```とは違い、```visibility: hidden;```は要素が**見えないだけでしっかり存在**します。
存在するということは、**その要素の大きさ分の空白ができる**ということです。

### レイアウトのくびきから解き放つ positionプロパティ
さて参ったね。
エラーを表示するには```visibility: hidden;```しか使えない。けれどこれを使ったらレイアウトが崩れる。
レイアウトを直すには```display: none;```を使うのが一番だけど、これを使うとエラーが出なくなる。

どちらの要件も維持するには、本来のラジオボタンを**レイアウトのくびきから解き放つ**しかない。
それが```position: absolute;```という設定。

HTMLの場合、レイアウトは相対で作られます。同じレイヤー上に存在するので、要素同士は隣り合っています。
しかし、```position: absolute;```を設定した要素は、他の要素とは異なるレイヤーに配置され、ブラウザの**左上を原点とした絶対値**でレイアウトを制御することが可能になります。
イメージとしては、相対でレイアウトされている他要素の上に浮かんでいる感じです。

ただ、フリーダムすぎると制御不能になるので、**従うべき親分**を教えます。

毎回ブラウザの左上を親分にされると制御が難しいので、親要素に```position: relative;```を与えて「**ワタシが親分です**」と教えてあげると、素直に従ってくれます。
この場合、```position: relative;```を持つ親要素の左上を原点として絶対値でレイアウトを作るので、制御は非常に簡単になります。

今回のレイアウトの都合上、上位の要素```<article class="contents">```に```text-align: center;```があるため、何もせずにいるとチェックボックスは画面の中央に配置されます。
![位置指定ある・なし](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44795/cfd0cfc5-65f4-afe5-3730-2c6f2e3e7265.jpeg)
[jQuery Validation Engine](https://github.com/posabsolute/jQuery-Validation-Engine)は```input```要素にエラーを表示します。
そのため、```position: absolute;```だけでなく、```left```と```top```に数値を入れて、擬似的に作り上げたラジオボタンの近くにレイアウトされるようにし、あたかもエラーが疑似ラジオボタンの上に出ている風を装います。

参考：[position - CSS: カスケーディングスタイルシート | MDN](https://developer.mozilla.org/ja/docs/Web/CSS/position)

:xmas-tree: :santa: :xmas-tree: :santa_tone1: :xmas-tree: :santa_tone2: :xmas-tree: :santa_tone3: :xmas-tree: :santa_tone4: :xmas-tree: :santa_tone5: :xmas-tree: 


レイアウトもどうにかなったし、デフォルトの機能だけならこれでいいんです。バリデーションも走るし。エラーテキストも変更できるし。
でも、まだ足りない。ちょっと仕様が足りないから、このままじゃダメ。

…自ら面倒くさい地獄を呼び込むことに。

[^いにしえのテーブルコーディング時代]: [若い世代が知らない2000年代のHTMLコーディングの地獄](https://ics.media/entry/17960/)…これをお読みいただければ、地獄加減はわかっていただけるであろう。バグやハックで朝までコースはザラ。

# ラジオボタンと入力フォームを連動させたい問題
連絡方法の項目に関しては、以下の事がしたいなーって思います。

+ **項目を選択したら入力フォームに入力させたい（項目未選択の場合は入力フォームもDisabled）**
+ **別の項目を選択したら入力した内容を消したい（見えなくしたい）**

HTMLは手っ取り早いですな。初期値は```disabled```にしておきます。

```html:HTML
<div class="contactForm_desc_comments">
　　<input type="radio" name="otherContact" value="電話番号" id="approach02"><label for="approach02">電話番号</label>
　　<div class="contactForm_desc_comments_input">
　　　　<input type="tel" name="otherContactComent" class="validate[condRequired[approach02]]" disabled="disabled">
　　</div>
</div><!-- /.contactForm_desc_comments -->
```

このままだと永遠に```disabled```なので、jQueryの力を借ります。

```js:Javascript
//ラジオボタンと入力エリア連動
$("[name='otherContact']").change(function(){
  if ($("input:radio[id='approach02']:checked").val() == "電話番号") {
      $("#approach02").nextAll(".contactForm_desc_comments_input").children("input[type=tel]").removeAttr("disabled");
    } else {
      $("#approach02").nextAll(".contactForm_desc_comments_input").children("input[type=tel]").attr("disabled", "disabled");
  }

  if ($("input:radio[id='approach03']:checked").val() == "その他") {
      $("#approach03").nextAll(".contactForm_desc_comments_input").children("input[type=text]").removeAttr("disabled");
    } else {
      $("#approach03").nextAll(".contactForm_desc_comments_input").children("input[type=text]").attr("disabled", "disabled");
   }
}).trigger('change'); 

```
チェックボックスが対象の場合は、こんな書き方で動くはず（今回のソースコードでは検証していない）。

```js:Javascript
$(".contactForm_desc_comments").change(function(){
  $("input[type=text]").attr("disabled", "disabled"); 
  $(":checked").nextAll(".contactForm_desc_comments_input").children("input[type=text]").removeAttr("disabled");
}).trigger("change");
```

これをやらず、わざわざ```val() == "電話番号"```みたいな書き方をしてるのは、ラジオボタンの場合、他の項目を執拗にタップすると関係ない入力フォームがActiveになるからw
テスト中は、執拗にタップし続けるのは大事ですねー:rolling_eyes:

ここまででもいいんですが、入力フォームに記入した後に別の項目を選択してDisabledになっても、入力された文字が見えちゃいますw
ここはCSSで簡単に消します。

```css:CSS
.contactForm_desc input[disabled="disabled"] {
    color: transparent; 
}
```
透明にしただけなので、再び項目を選択すると入力内容が復活します:stuck_out_tongue_winking_eye:
jQueryで消しちゃってもいいんですが、この時はテストする時間が少な過ぎたので「見えなくする(透明にする)」だけにしました。
項目がActiveにならない限り、入力フォームは```disabled="disabled"```なので、内容が送信されることはありません。
![ラジオボタン挙動サンプル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44795/d08e1561-6a41-1b5f-f1ae-d40dc7cb79aa.gif)

#連打禁止
イタズラされるのが嫌なので、できる限り連打できないようにしたい。
「送信」ボタンをタップした後に、ボタンカラーを透過して「押しましたよ」という状況変化をつけるのもいいし、```pointer-events```プロパティ使って、1度タップしたらポインターイベントを無効にしちゃってもいい。

ただ、```pointer-events```は注意が必要。

バリデーションが走ってエラーになった後、なにかのタイミングで復帰させないと、必須項目を全て入力しても内容を送れないwwww

```js:Javascript
//連打緩和
$("#submit").on("click", function(){
  $(this).css({"opacity":" .5" ,"pointer-events": "none"});
});

$("form").change(function(){
  //フォーム内の要素に変更があると適用
  $("#submit").css({"opacity":" 1" ,"pointer-events": "auto"});
});
```
バリデーションが走った後は、未入力を無くすためにタップなり記入なりするはずなので、フォーム内の要素に変更があったら、ボタンの透過をやめて、ポインターイベントを有効にします。

#一応動くなにか
[jQuery Validation Engine](https://github.com/posabsolute/jQuery-Validation-Engine)は非常に有名なバリデーションプラグインで、Qiita上でも様々な人が記事を書いています。
しかし、フォーム部品に装飾を施した場合の事例は少ないように思います。

装飾を施していないフォームで利用する場合は、そう大きな問題はないと思いますが、色々装飾をした瞬間、ドツボにハマる可能性は極めて高いです。
そして、だいたいが「**しょうもない理由**」でハマってしまうので、判明した時の落ち込み方はひとしおですw

CodePenで検索すると古いバージョンしか出てこず、[jQuery Validation Engine](https://github.com/posabsolute/jQuery-Validation-Engine)のCDNを探すのにちょっと苦労しましたorz
CDNは[jsDelivr](https://www.jsdelivr.com/)にあったので助かった:innocent:
<p class="codepen" data-height="600" data-default-tab="html,result" data-slug-hash="oNGGvrJ" data-user="mrd-takahashi" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/mrd-takahashi/pen/oNGGvrJ">
  Contact Form HTML</a> by Mari Takahashi (<a href="https://codepen.io/mrd-takahashi">@mrd-takahashi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>