jQueryを用いた増殖フォームについて(および:hiddenセレクタの挙動に関する注意事項)
HTMLでフォームを作る際、初めから必要個数が分かっている場合であれば、静的なHTMLファイル一枚用意すれば事は足りるが、ユーザ操作によって動的にフォームを増やせるようにしたい、といった場合には、JavaScriptによってHTMLをDOM操作して処理する必要がある。
そういった場合に、jQueryを用いることで、かなり容易に実装することが可能となる。
今回は、そのテストケースとしての実装例を挙げ、その挙動について考察をしてみる。
単純なフォームの複製
copyボタンを押すことで、フォームのまとまり(ブロック)が複製されていくという単純な例を、以下の通りに実装した。
[test1.html] <html> <head> <script type="text/javascript" src="jquery-1.4.2.min.js"></script> <script type="text/javascript"> <!-- // フォームの複製を行う関数を定義 var copy_block = function(i) { var increament_id = function(name,id) { $("#cover"+id+" ."+name).attr("id", name+id); }; var target = $("#cover"+(i-1)); target.clone().insertAfter(target).attr("id","cover"+i); increament_id("text",i); increament_id("field",i); increament_id("select",i); $("#field"+i).val(i); }; // イベントを設定 $(function() { $("#button1").click(function() { var i=1; while($("#cover"+i).length != 0) { i++; } copy_block(i); }); }); //--> </script> <title>test</title> </head> <body> <div id="cover1"> <input type="text" class="text" id="text1" /> <input type="hidden" class="field" id="field1" value="1" /> <select class="select" id="select1"> <option value="0">--</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> </div> <input type="button" id="button1" value="copy" /> </body> </html>
まず、フォームを覆うdiv要素にcover1というIDを付け、その中にtextフォームとhiddenフォーム、selectフォームを配置した。(IDはそれぞれ”text1”、”field1”、”select1”とする。)
目的の動作は、copyボタンをクリックすることで、div要素が元のdiv要素の後ろに複製されていくという内容。
実際の処理内容は、まず、copy_blockという関数を定義し、この関数をcopyボタンのクリックイベントに割り当てる。この関数を呼び出す際、新規に作成するdiv要素の通し番号(IDに付加するための数値)を引き渡す。
copy_block関数では、直前のdiv要素をセレクタに指定し、clone()によって複製、insertAftter()で配置していく。複製の後、各フォームのIDを新しい通し番号に更新する。
この更新処理のため、各フォームのclassには、IDの値から通し番号を除いた名前を予めclass名として持たせておき、複製完了直後、”複製したdiv要素以下の、指定class値を持つ要素”という形でこのclass名をセレクタに指定し、”class名 + 新しい通し番号”という形式で値を作成し、IDを更新していく。
ex) <input type="text" name="text1" id="text1" class="text" /> としておき、 $("cover"+i+" .text").attr("name", "text"+i).attr("id", "text"+i); とすることで、 <input type="text" name="text2" id="text2" class="text" /> とすることができる。
因みに、[test1.html]のコード例では、IE以外のブラウザでフォームを複製する時に、select要素のselected指定されたoption情報が引き継がれない。(なぜIEは引き継ぐのかは不明。)
そのため、select要素のseleced指定されたのoption値については、別途、複製処理を行う必要がある。
[test2.html] <html> <head> <script type="text/javascript" src="jquery-1.4.2.min.js"></script> <script type="text/javascript"> <!-- var copy_block = function(i) { var increament_id = function(name,id) { $("#cover"+id+" ."+name).attr("id", name+id); }; // select要素のselected指定されたoptionの情報を複製するための関数を定義 var set_select_vals = function(i) { var prev_vals = []; $("#cover"+(i-1)+" select").each(function() { prev_vals.push( $(this).val() ); }); $("#cover"+i+" select").each(function() { $(this).val( prev_vals.shift() ); }); }; var target = $("#cover"+(i-1)); target.clone().insertAfter(target).attr("id","cover"+i); increament_id("text",i); increament_id("field",i); increament_id("select",i); $("#field"+i).val(i); set_select_vals(i); }; $(function() { $("#button1").click(function() { var i=1; while($("#cover"+i).length != 0) { i++; } copy_block(i); }); }); //--> </script> <title>test</title> </head> <body> <div id="cover1"> <input type="text" class="text" id="text1" /> <input type="hidden" class="field" id="field1" value="1" /> <select class="select" id="select1"> <option value="0">--</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> </div> <input type="button" id="button1" value="copy" /> </body> </html>
上記のコードではset_select_vals関数を追加した。
この関数では、複製元のselected指定されたoptionのvalue値を取得し、それを新しいselect要素にも適用する、という処理を行っている。
なお、selected指定されたoptionのvalue値の取得およびselected指定は、共にval()によって行っている。
空のフォームを追加する
より実際的なニーズに対応するためには、単純な複製だけでなく、設定した値をクリアした状態での複製―すなわち、フォームの新規追加も可能にしておく必要があると思われる。
今回の例では、まず先に複製を行い、その後に値を初期化する、という方法を取る。
[test3.html] <html> <head> <script type="text/javascript" src="jquery-1.4.2.min.js"></script> <script type="text/javascript"> <!-- var copy_block = function(i) { var increament_id = function(name,id) { $("#cover"+id+" ."+name).attr("id", name+id); }; var set_select_vals = function(i) { var prev_vals = []; $("#cover"+(i-1)+" select").each(function() { prev_vals.push( $(this).val() ); }); $("#cover"+i+" select").each(function() { $(this).val( prev_vals.shift() ); }); }; var target = $("#cover"+(i-1)); target.clone().insertAfter(target).attr("id","cover"+i); increament_id("text",i); increament_id("field",i); increament_id("select",i); $("#field"+i).val(i); set_select_vals(i); }; // 値を初期化した状態で複製を行う関数を定義 var add_block = function(i) { var init_vals = function(i) { $("#cover"+i+" :text").val(''); $("#cover"+i+" :hidden").val(''); $("#cover"+i+" select").val('0'); }; copy_block(i); init_vals(i); }; $(function() { $("#button1").click(function() { var i=1; while($("#cover"+i).length != 0) { i++; } copy_block(i); }); $("#button2").click(function() { var i=1; while($("#cover"+i).length != 0) { i++; } add_block(i); }); }); //--> </script> <title>test</title> </head> <body> <div id="cover1"> <input type="text" class="text" id="text1" /> <input type="hidden" class="field" id="field1" value="1" /> <select class="select" id="select1"> <option value="0">--</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> </div> <input type="button" id="button1" value="copy" /> <input type="button" id="button2" value="new" /> </body> </html>
[test2.html]から新たにadd_block関数を定義し、これをnewボタンのクリックイベントに割り当てた。
add_block関数では、フォームの値を初期化するためのinit_vals関数を定義し、それをcopy_block関数を実行した後、実行する。
[test3.html]のコード例でのセレクタ設定は、Firefoxにおいては問題なく動作する。
ただし、safariおよびGoogle ChromeのWebKit系ブラウザでは、初期化時にselect要素の初期値が正しくセットされず、option要素が無選択の状態で追加されてしまう。
さらにこの状態で、optionの選択をしてcopyを行うと、今度はoption要素の最大値が設定された状態で複製されてしまう。
これはIEにおいてもほぼ同じ現象が発生する。(より正確に述べると、IEではそもそも値の初期化が行われない)
この問題は、以下のようにすることで解決できる。
[test4.html] <html> <head> <script type="text/javascript" src="jquery-1.4.2.min.js"></script> <script type="text/javascript"> <!-- var copy_block = function(i) { var increament_id = function(name,id) { $("#cover"+id+" ."+name).attr("id", name+id); }; var set_select_vals = function(i) { var prev_vals = []; $("#cover"+(i-1)+" select").each(function() { prev_vals.push( $(this).val() ); }); $("#cover"+i+" select").each(function() { $(this).val( prev_vals.shift() ); }); }; var target = $("#cover"+(i-1)); target.clone().insertAfter(target).attr("id","cover"+i); increament_id("text",i); increament_id("field",i); increament_id("select",i); $("#field"+i).val(i); set_select_vals(i); }; var add_block = function(i) { var init_vals = function(i) { $("#cover"+i+" :text").val(''); // :hiddenの前にinputを指定 $("#cover"+i+" input:hidden").val(''); $("#cover"+i+" select").val('0'); }; copy_block(i); init_vals(i); }; $(function() { $("#button1").click(function() { var i=1; while($("#cover"+i).length != 0) { i++; } copy_block(i); }); $("#button2").click(function() { var i=1; while($("#cover"+i).length != 0) { i++; } add_block(i); }); }); //--> </script> <title>test</title> </head> <body> <div id="cover1"> <input type="text" class="text" id="text1" /> <input type="hidden" class="field" id="field1" value="1" /> <select class="select" id="select1"> <option value="0">--</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> </div> <input type="button" id="button1" value="copy" /> <input type="button" id="button2" value="new" /> </body> </html>
”:hidden”を指定する際、input要素の指定もしておくなど、より範囲を限定する形でセレクタを設定しないと、予期せぬ影響を及ぼすようである。
実際、この”:hidden”セレクタは、input要素のhiddenタイプに限らず、他の不可視状態にある要素もマッチしてしまうようである。(それがなぜselect要素に影響を及ぼすかは不明)
以上で、増殖フォームの実装が完了する。
ここからさらに、1ブロックあたりのフォームの数を増やすなど、カスタマイズしていくことで、より使い勝手の良いものとなっていく。
フォームの数が増えてきた場合には、ID名の一覧を配列に持たせ、それをループで読み出して処理するようにすることで、より柔軟に対応することが出来る。