私事になりますが、
最近仕事をwebコーダーから
WEBアプリのバックエンドエンジニアに鞍替えしまして、
その影響でHTMLフォームを利用したデータの受け渡し処理を書くことが増えました。
そして、その際に手間のかかる実装の一つに
「入力者が自由に数を増減できる入力フィールド」の実装があります。

↑こんな感じのやつですね。
一見簡単そうに見えますが、
POST送信後のデータ処理まで考慮すると
これが意外とめんどくさいのです。
- POST送信後の処理を考慮してname=”hoge[1][title]”みたいな形でnameを連番指定したいけど、その場合[1]の数字部分を増減の度に差し替えないといけない
- フィールド追加の際にコピー元になるHTMLはどこに置くか
- 複数の増減フィールドがあっても干渉せずに動作させられるか
などなど…
思いのほか考慮しないといけないことが多く、
油断するとすぐ泥沼にはまりかねない難敵なんですよね。
そこで本日ご紹介するのは、
この増減可能フィールドの実装を
極力シンプルな形でまとめたjQuery(+HTMLとCSS)のコードです。
上述した課題を概ねクリアし、
必要最低限の処理のみに絞ることで
カスタマイズの土台としても活用できるものになることを理想として作成しています。
POST送信後は配列の形式で送信データを受け取れるように、
name属性の動的な書き換えにも対応しています。
それでは、見ていきましょう。
デモとソースコード
<div class="wrap">
<h2>増減可能フィールドつき入力フォームのサンプル</h2>
<form id="edit-content" class="myform" action="/hogehoge" method="post" enctype="multipart/form-data">
<!-- input text -->
<div class="form-group">
<p for="" class="form-group__title"> 追記事項 </p>
<div class="js-ff-add __lined expendable-fields" data-count="" data-js-ff-slug="others" data-js-ff-count="3">
<div class="js-ff-add-wrapper">
<div class="js-ff-add-wrapper-wrapper">
<div class="cell js-ff-add-item js-dynamic-required__parent" data-js-ff-count="1" data-js-ff-slug="others">
<div class="js-ff-add-item-wrapper p-add_group">
<p>タイトル</p>
<textarea name="others[1][q]" class="my-textarea others others-title" cols="30" rows="5">1つ目の追記事項のタイトル</textarea>
<p>内容</p>
<textarea name="others[1][a]" class="my-textarea others others-body" cols="30" rows="5">1つ目の追記事項の内容</textarea>
<div class="js-ff-add-item-buttons">
<button type="button" id="" class="btn btn_brand bottom-delete btn-red js-ff-add-remove">削除</button>
</div>
</div>
</div>
<div class="cell js-ff-add-item js-dynamic-required__parent" data-js-ff-count="1" data-js-ff-slug="others">
<div class="js-ff-add-item-wrapper p-add_group">
<p>タイトル</p>
<textarea name="others[2][q]" class="my-textarea others others-title" cols="30" rows="5">2つ目の追記事項のタイトル</textarea>
<p>内容</p>
<textarea name="others[2][a]" class="my-textarea others others-body" cols="30" rows="5">2つ目の追記事項の内容</textarea>
<div class="js-ff-add-item-buttons">
<button type="button" id="" class="btn btn_brand bottom-delete btn-red js-ff-add-remove">削除</button>
</div>
</div>
</div>
</div>
<div class="js-ff-add-button-wrap">
<button type="button" class="btn btn_brand js-ff-add-button">追加するにはここをクリック</button>
</div>
</div>
</div>
</div>
<div class="form-group">
<p for="" class="form-group__title"> 質問/回答 </p>
<div class="js-ff-add __lined expendable-fields" data-count="" data-js-ff-slug="faq" data-js-ff-count="2">
<div class="js-ff-add-wrapper">
<div class="js-ff-add-wrapper-wrapper">
<div class="cell js-ff-add-item js-dynamic-required__parent" data-js-ff-count="1" data-js-ff-slug="faq">
<div class="js-ff-add-item-wrapper p-add_group">
<p>質問</p>
<textarea name="faq[1][q]" class="my-textarea faq faq-a" cols="30" rows="5">1つ目の質問の内容</textarea>
<p>回答</p>
<textarea name="faq[1][a]" class="my-textarea faq faq-q" cols="30" rows="5">1つ目の回答の内容</textarea>
<div class="js-ff-add-item-buttons">
<button type="button" id="" class="btn btn_brand bottom-delete btn-red js-ff-add-remove">削除</button>
</div>
</div>
</div>
</div>
<div class="js-ff-add-button-wrap">
<button type="button" class="btn btn_brand js-ff-add-button">追加するにはここをクリック</button>
</div>
</div>
</div>
</div>
<div class="send-button">
<button type="submit" id="submit-all" class="submit-btn btn btn-green">送信</button>
</div>
</form>
</div>
<div id="add_originals" class="js-ff-add-originals">
<div class="js-ff-add-wrapper" data-copy-original-slug="others">
<div class="cell js-ff-add-item js-dynamic-required__parent" data-js-ff-slug="others">
<div class="js-ff-add-item-wrapper p-add_group">
<p>タイトル</p>
<textarea name="others[%%count%%][q]" class="my-textarea others others-title" cols="30" rows="5"></textarea>
<p>内容</p>
<textarea name="others[%%count%%][a]" class="my-textarea others others-body" cols="30" rows="5"></textarea>
<div class="js-ff-add-item-buttons">
<button type="button" id="" class="btn btn_brand bottom-delete btn-red js-ff-add-remove">削除</button>
</div>
</div>
</div>
</div>
<div class="js-ff-add-wrapper" data-copy-original-slug="faq">
<div class="cell js-ff-add-item js-dynamic-required__parent" data-js-ff-slug="faq">
<div class="js-ff-add-item-wrapper p-add_group">
<p>質問</p>
<textarea name="faq[%%count%%][q]" class="my-textarea faq faq-a" cols="30" rows="5" data-parsley-required="false" data-js-dynamic-required-type="required"></textarea>
<p>回答</p>
<textarea name="faq[%%count%%][a]" class="my-textarea faq faq-q" cols="30" rows="5" data-parsley-required="false" data-js-dynamic-required-type="required"></textarea>
<div class="js-ff-add-item-buttons">
<button type="button" id="" class="btn btn_brand bottom-delete btn-red js-ff-add-remove">削除</button>
</div>
</div>
</div>
</div>
</div>
body{
margin: 0;
}
.myform{
max-width: 600px;
box-sizing: border-box;
padding: 20px;
border: 1px dashed #bbb;
}
.my-textarea{
width: 100%;
padding: 10px;
box-sizing: border-box;
border: 1px solid #999;
}
.send-button{
padding-top: 20px;
text-align: right;
}
.add-item{
box-sizing: border-box;
padding: 20px;
background-color: #fff;
}
.form-group__title{
margin-bottom: 10px;
font-size: 22px;
font-weight: bold;
}
h2{
font-size: 28px;
margin: 0 0 40px;
}
.wrap{
padding: 20px;
box-sizing: border-box;
letter-spacing:0.1em;
}
.js-ff-add-button-wrap{
text-align: right;
}
.js-ff-add-item-buttons{
padding-top: 10px;
}
.form-group{
margin-bottom: 30px;
}
.form-group:last-child{
margin-bottom: 0px;
}
.btn{
background-color: #8db3d6;
border: none;
color: #FFFFFF;
padding: 12px 32px;
text-align: center;
-webkit-transition-duration: 0.4s;
transition-duration: 0.4s;
margin: 16px 0 !important;
text-decoration: none;
font-size: 16px;
cursor: pointer;
}
.btn.btn-red{
background-color: #d68d8d;
}
.btn.btn-green{
background-color: #8dd696;
}
.expendable-fields{
background-color: #E7E9EB;
}
.wrap{
color:#666666;
}
.p-add_group:last-child{
margin-bottom: 0;
}
.js-ff-add-item{
margin-bottom: 20px;
}
.js-ff-add-item:last-child{
margin-bottom: 0;
}
.js-ff-add-wrapper.__old{
margin-bottom: 20px;
}
.js-ff-add-item-buttons{
text-align: right;
}
.js-ff-add{
padding: 20px;
}
.di-textarea-tall{
height: 120px;
}
.js-ff-add-item .form-group{
margin-bottom: 2.6rem;
}
.js-ff-add-wrapper-wrapper{
margin-bottom: 20px;
}
.js-ff-add-button{
text-align: right;
}
.js-ff-add-originals{
display: none;
}
//増減可能フィールドの処理
$(function(){
var counts = []; //各.js-ff-addのフィールドの個数が何個かを記憶しておく配列
var initial_counts = []; //最初に取得した状態のcountを記憶しておく。removeで消しすぎないように
var originals = []; //コピーするフィールドの原型
$('.js-ff-add').each(function(){
var target = $(this); //.js-ff-add
var target_new = $(this).children('.js-ff-add-wrapper.__new'); //雛形
var count = target_new.find('.js-ff-add-item').data('js-ff-count'); //今何個めか。初期値は.js-ff-add-wrapper.__new'からとる
var slug = target_new.find('.js-ff-add-item').data('js-ff-slug'); //置換対象の区別用slug
counts[slug] = target_new.find('.js-ff-add-item').data('js-ff-count');
initial_counts[slug] = target_new.find('.js-ff-add-item').data('js-ff-count');
originals[slug] = target_new.html();
});
$(document).on('click','.js-ff-add-button',function(){
var slug = $(this).parents('.js-ff-add').attr('data-js-ff-slug');
var count = $(this).parents('.js-ff-add').attr('data-js-ff-count');
var ef_default = $('#add_originals .js-ff-add-wrapper[data-copy-original-slug="'+slug+'"]').html(); //雛形のHTML
regexp = new RegExp('%%count%%', 'g');
var add = ef_default.replace(regexp, count);
$(this).parents(".form-group").find('.js-ff-add-wrapper-wrapper').append(add);
$(this).parents('.js-ff-add').attr('data-js-ff-count',parseInt(count)+1);
});
$(document).on('click','.js-ff-add-remove',function(){
var slug = $(this).parents('.js-ff-add').attr('data-js-ff-slug');
var count = $(this).parents('.js-ff-add').attr('data-js-ff-count');
$(this).parents(".js-ff-add-item").remove();
});
});
使い方
基本はコピペしたソースをいじりながら
動作を確認してもらうのが早いかと思いますが、
簡単に使い方を解説しておきます。
jsコードの読み込み後に必要な設定は以下です。
増減させる対象
増減を行いたいフィールドの各要素に以下のクラスをつけます。
.js-ff-add
増減対象全体を囲む親のタグにつけます。
この中で要素が増えたり減ったりします。
また、.js-ff-addは以下のdata属性を指定します。
・data-js-ff-slug … 各増減フィールドのスラッグ(管理用のニックネーム)。
・data-js-ff-count … 各増減フィールドが現在持っているフィールドの個数の管理用。フィールドの増減時は、個々の数値を見て追加するフィールドのname属性の連番を適宜書き換えます。
.js-ff-add-wrapper
減ったり増えたりする要素そのものにつけます。
通常は、.js-ff-addの直下の子要素に就けることになるかと思います。
.js-ff-add-button
これを押したら、要素を一個増やすよというボタンにつけます。
.js-ff-add-remove
これを押したら、要素を一個減らすよというボタンにつけます。
増減させる対象の雛形
フィールド追加の際に参照する
ひな形用のHTMLは予めHTML内にdisplayをnoneにして仕込んでおきます。
上記のサンプルコードで言うと
.js-ff-add-originals以下がひな形用のコード置き場になっています。
ひな形用コードには以下の要素を持たせてください
・data-copy-original-slug … 対応する増減フィールドのdata-js-ff-slugと同じ値を持たせて、コピーの際の目印とするものです。
・%%count%% … この文字列が追加時に現在の増減フィールドのカウントの数値置き換えられます。例えばname=”faq[%%count%%][q]”のように書いておけば、コピー時にはname=”faq[2][q]”のように変換されます。

↑デベロッパーツールでソースを見て頂くと、各textareaのnameにそれぞれ個別の数字が割り振られていることが確認頂けるかと思います。
おわりに
以上、増減フィールド用コードとその解説でした。
このコードで、私のような苦労をする人が少しでも減れば嬉しいです。それでは。



コメント