jQueryを用いた増殖フォームについて(および:hiddenセレクタの挙動に関する注意事項)

HTMLでフォームを作る際、初めから必要個数が分かっている場合であれば、静的なHTMLファイル一枚用意すれば事は足りるが、ユーザ操作によって動的にフォームを増やせるようにしたい、といった場合には、JavaScriptによってHTMLをDOM操作して処理する必要がある。
そういった場合に、jQueryを用いることで、かなり容易に実装することが可能となる。

今回は、そのテストケースとしての実装例を挙げ、その挙動について考察をしてみる。

環境

今回の実装およびテストは以下の環境にて行った。

ライブラリ
  • jQuery1.4.2
ブラウザ

単純なフォームの複製

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 ChromeWebKit系ブラウザでは、初期化時に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名の一覧を配列に持たせ、それをループで読み出して処理するようにすることで、より柔軟に対応することが出来る。