最近PHPでユーザーが出力した文章を保存する機能を自作しており、複数のinputをjs一つのinputにまとめてから送信ボタンを押してPHP側にデータを渡す作業を行っている。
より詳しく話すと、フロント側はformとinputを使ってレポートや論文の出典を誰でも簡単に作成できるジェネレーターを作っているところだ。
See the Pen Untitled by Tomizawa (@masaya_coding) on CodePen.
しかし、formを増やせば増やすほどJSのコードも増えていき、乱雑になってしまう。
そこで今回は複数のformタグ毎に処理を書くのではなく、なるべくコードを統一しながら各々の処理をまとめて書く方法を解説したいと思う。
ちなみに、今回実装していく内容はざっくりと下記の通りだ。
- ①複数のformタグ内の処理をループ処理でなるべくひとつにまとめる
- ②入力情報をリアルタイムで表示
- ③クロージャとreturnで②イベント内に関数を作って発火させる
Table of Contents
HTML
まずはHTMLの設定から行っていく。
今回行う作業としては、formとinputを使ってレポートや論文の出典を誰でも簡単に作成できるジェネレーターを作ってみる。
<form action="save.php" method="GET" class="myForm">
<h2>作者が一人の場合</h2>
<input type="text" name="auther01" class="auther01" placeholder="作者名01" oninput="updateComplete1()">
<input type="hidden" name="auther02" class="auther02" placeholder="作者名02">
<input type="hidden" name="auther03" class="auther03" placeholder="作者名03">
<input type="text" name="date" class="date" placeholder="発行日" oninput="updateComplete1()">
<input type="text" name="name" class="name" placeholder="本の名前" oninput="updateComplete1()">
<input type="text" name="publisher" class="publisher" placeholder="出版社" oninput="updateComplete1()">
<br>
<input type="text" name="complete" class="complete">
<input type="submit" value="保存" class="submit" disabled>
</form>
<form action="save.php" method="GET" class="myForm">
<h2>作者が複数の場合</h2>
<input type="text" name="auther01" class="auther01" placeholder="作者名01" oninput="updateComplete2()">
<input type="text" name="auther02" class="auther02" placeholder="作者名02" oninput="updateComplete2()">
<input type="text" name="auther03" class="auther03" placeholder="作者名03" oninput="updateComplete2()">
<input type="text" name="date" class="date" placeholder="発行日" oninput="updateComplete2()">
<input type="text" name="name" class="name" placeholder="本の名前" oninput="updateComplete2()">
<input type="text" name="publisher" class="publisher" placeholder="出版社" oninput="updateComplete2()">
<br>
<input type="text" name="complete" class="complete">
<input type="submit" value="保存" class="submit" disabled>
</form>
上記の通りにパターンのジェネレーターを作るが、特に下記の点は後のプログラムにも直結するので、意識しておいてもらいたい。
- fromタグのクラスは全て「myForm」で統一
- 各inputに「oninput=関数」をセットし、JSを実行
- 保存ボタンは本の名前が抜けている場合は押せない
各formをループ処理してnameとsubmit要素を取得
まずはmyForm クラスを持つすべてのフォームに入っているname(本の名前)、とボタン要素に対してイベントリスナーを追加する処理から行う。
これは各form内にある「本の名前」が入っていない限り、ボタンを押せなくする処理を全てのformに適応できるように書いてみる。
var forms = document.getElementsByClassName("myForm"); //myForm クラスを持つすべてのフォームに対してイベントリスナーを追加
for (var i = 0; i < forms.length; i++) { // "myForm" クラスを持つすべてのフォームに対してループ処理を行う
var nameInput = forms[i].getElementsByClassName("name")[
0]; //iの中にはforms.length; i++の処理によって、myFormというクラスをもつ要素(フォーム)がコレクションとして格納される
//ここでの0はforms[i]に格納されたmyFormというクラスをもつ要素の中身にある最初の要素を意味する。なので[1]を使用せずとも一回の処理で各コレクションにアクセス可能。
var saveButton = forms[i].getElementsByClassName("submit")[0];
// コレクション=getElementsByClassName("name") のようにクラス名が "name" である要素の集合を取得し、コレクションとして扱われる。今回の場合はformsがコレクション(集合体)
以下省略~
for文でformがある限り繰り返し処理を行う。
iの中にはforms.length; i++の処理によって、myFormというクラスをもつ各要素(フォーム)がコレクションとして格納される。
三行目ではフォームが格納されたmyFormというクラスをもつ要素 forms[i] に対して.getElementsByClassNameメソッドを使って、フォーム内にある最初のnameクラスを取り出すのだ。
それをループしていることにより、各フォームに同じ処理を繰り返してくれる(その下のボタンの部分も同様だ)。
ループ処理をしない場合は[0][1][2]…と各フォームごとに一つ一つ似たような処理をするため、コードが非常に乱雑になるので、この書き方がオススメだ。
addEventListenerでリアルタイムイベントを設定
ここまででnameとボタン要素をコレクションとして格納し、それを「nameInput」・「saveButton」という変数に格納した。
今度はこれを使ってinput(作者名)に要素が入った瞬間にボタンが活性化するリアルタイムイベントを行う準備をしていく。
nameInput.addEventListener("input", createInputListener(nameInput, saveButton)); // nameInput の値が変化したときに、createInputListener 関数が実行される
}
以下省略~
念のためここでのaddeventlisnerの使い方を簡単にまとめておく。
- ①nameInput=nameのinputに
- ②.addEventListener=リアルタイムで変化があった時
- ③(“input”=input要素の場合
- ④createInputListene(nameInput, saveButton));=createInputListener関数に nameInput と saveButton を引数としてセットして実行
これでinput要素とボタンを使った処理を行う準備が整ったので、最後にその処理内容を書いていく。
処理内容を関数にまとめる
引き続き解説を行う。
function createInputListener(input, button) {
//return function() { ... } の部分は、イベントリスナーとして使用するための関数
return function() { //ここではreturn文を無名関数に使って結果を呼び出し元に返すことで、イベントリスナーの処理として再利用できている(タイミング的には呼び出しもとで処理が行われる前になる)
//内部の関数を返すことで、外部のスコープにあるnameInputとsaveButtonの値を保持しつつ、イベントが発生した際に正しくボタンの状態を制御
button.disabled = !input.value;
// input の値が空であれば、ボタンを無効化(disabled = true)
// input の値が入力されていれば、ボタンを有効化(disabled = false)
};
}
createInputListenerという関数の引数は、先ほどイベントリスナーでセットした関数で、これを呼び出すことになる。
気負付けたいことは関数の中に function() という無名関数をセットし使用していることだ。
これはクロージャと呼ばれているが、これがないと下記の理由によりイベントリスナーは処理されない。
- ①イベントリスナーはリアルタイム表示のため、ユーザーの使い方によってこの関数よりも遅れて呼び出される(実行タイミングがユーザー次第)
- ②イベントリスナーが呼び出される時点で外部の変数(この場合はnameInputとsaveButton)の値が必要だが、リアルタイム処理なので直接引数として渡すことは不可能
- ③つまり、イベントリスナーの実行時でも createInputListener() 内の関数の値は固定されてしまっているので、新しい引数が渡ってこない
- ④そうなると引数「nameInput」と「saveButton」がセットされないまま実行されるので、その値をもとに処理ができない
つまり、イベントリスナーの処理が「createInputListener関数」よりも後に走るため、リアルタイムで入力された値がセットできないまま関数の処理が行われてしまうのだ。
解決策はクロージャとreturn
上記の通り、この問題は関数の中にもう一つ内部関数をセット(クロージャ)で解決することができる。
- ①クロージャ(今回は関数の中に関数)を使用することで、「イベント内に別の関数を作る」ことができる
- ②この内部関数は外部スコープ(nameInputとsaveButton)の変数を参照するため、イベントが発生した瞬間の変数の最新の値を参照し、保存できる
- ③その値に基づいて処理が行われる
- ④つまりイベントリスナーに関数と引数をセットしてを使う際は、クロージャは必須
そして呼び出し元のイベントリスナーではこの関数の結果を呼び出し、関数外で新たな処理を行うため、returnを付ける必要がある。
returnの使い方は下記を参照するといいだろう。