JavaScript / jQueryでtableの行を「追加」「削除」「移動」「変更」させる方法

昨年のこととなりますが、仕事で開発しているシステムの中に、メールソフトなどによくある「フィルタリング設定」のような機能を実装しました。フィルタリングの条件をリストに追加し、リストを上下に移動することで優先順位を変更でき、さらに内容も変更でき、必要のないものは削除するといったものです。

今、この「リストを操作する機能」を使って、このブログに新しい機能を追加してみようと考えています。それに先立ち、思い出す意味も含めてブログにまとめてみることにしました。以下、参考になればと思います。

サンプル

まずは、どんな機能化ということで、サンプルを挙げておきます。要は「テーブルの行を思うがままにJavaScriptで操作してしまおう」といった機能となります。

< サンプル >

リスト選択リスト名リスト順追加/削除
A 上へ  下へ 

手っ取り早く機能を確認するには、以下の方法をお試しください。

  • まず「リスト選択」の列の選択肢から「B」を選択します。
     ⇒「リスト名」が「B」に変更されます。(変更)
  • 「追加/削除」の列の「+」ボタンを押します。
     ⇒行が追加されます。(追加)
  • 追加された行の「リスト順」の列の「↑」をクリックします。
     ⇒行が上へ移動します。(移動)
  • 移動した行の「リスト順」の列の「↓」をクリックします。
     ⇒行が下へ移動します。(移動)
  • 最後に、好きな行の「追加/削除」の列の「-」ボタンを押します。
     ⇒行が削除されます。(削除)

いかがでしょうか。まあ、よくある機能ですね。では、この機能の実装方法について以下にまとめておきたいと思います。

HTML

HTMLでは、以下の点がポイントとなります。

  • それぞれの機能を発生させるためのイベントハンドラをセットしておきます。
  • 操作の対象となる「行」の部分は<tbody>タグ内に記述するようにします(これが後々大事になります)。
  • JavaScript側ではDOMを使用しますが、ブラウザーによって「改行」をノードとして扱うものがあったりなかったりするので、一切改行を入れずに記述します。
<table id="p2146-table">

<thead>
<tr>
  <th>リスト選択</th>
  <th>リスト名</th>
  <th>リスト順</th>
  <th>追加/削除</th>
</tr>
</thead>

<!--▼改行しない▼-->
<tbody id="p2146-tbody"><tr><td><select onchange="changeList(this);"><option>A</option><option>B</option><option>C</option></select></td><td>A</td><td><img src="up.png" alt="↑" onclick="upList(this);" />上へ <img src="down.png" alt="↓" onclick="downList(this);" />下へ</td><td><input type="button" value="+" onclick="addList(this);" /> <input type="button" value="-" onclick="removeList(this);" /></td></tr></tbody>
<!--▲改行しない▲-->

</table>

JavaScript

JavaScriptでは、HTML内に記述したイベントハンドラによって呼び出される関数をそれぞれ定義します。

行を追加する

テーブル内の「+」ボタンが押されると、以下のaddList()関数が呼び出されます。処理内容としては、テーブルの1行目の「行(tr要素)」のクローンを作成し、それを「+」ボタンが押された行の下に追加します。

function addList(obj) {

  // tbody要素に指定したIDを取得し、変数「tbody」に代入
  var tbody = document.getElementById("p2146-tbody");
  // objの親の親のノードを取得し(つまりtr要素)、変数「tr」に代入
  var tr = obj.parentNode.parentNode;
  // tbodyタグ直下のノード(行)を複製し、変数「list」に代入
  var list = tbody.childNodes[0].cloneNode(true);
  // 複製した行の2番目のセルを指定し、変数「td」に代入
  var td = list.childNodes[1];
  // 複製した行の2番目のセルの内容を「A」に置き換え
  td.textContent = "A";
  // 複製したノード「list」を直後の兄弟ノードの上に挿入
  // (「tr」の下に挿入)
  tbody.insertBefore(list, tr.nextSibling);

}

行を削除する

テーブル内の「-」ボタンが押されると、以下のremoveList()関数が呼び出されます。処理内容としては、単純に「-」ボタンが押された行を削除します。

function removeList(obj) {

  // tbody要素に指定したIDを取得し、変数「tbody」に代入
  var tbody = document.getElementById("p2146-tbody");
  // objの親の親のノードを取得し(つまりtr要素)、変数「tr」に代入
  var tr = obj.parentNode.parentNode;
  // 「tbody」の子ノード「tr」を削除
  tbody.removeChild(tr); 

}

行を一つ上に移動させる

テーブル内の「↑」が押されると、以下のupList()関数が呼び出されます。処理内容としては、「↑」が押された行の上に「行が存在した場合」に、その行の上に挿入させます。

function upList(obj) {

  // tbody要素に指定したIDを取得し、変数「tbody」に代入
  var tbody = document.getElementById("p2146-tbody");
  // objの親の親のノードを取得し(つまりtr要素)、変数「tr」に代入
  var tr = obj.parentNode.parentNode;

  // もし「tr」の直前の兄弟ノード名が「TR」だった場合
  // (上に「行」が存在している場合)
  if(tr.previousSibling.nodeName === "TR") {
    // 「tr」を直前の兄弟ノードの上に挿入
    tbody.insertBefore(tr, tr.previousSibling);
  }

}

行を一つ下に移動させる

テーブル内の「↓」が押されると、以下のdownList()関数が呼び出されます。処理内容としては、「↓」が押された行の下に「行が存在した場合」に、その行の下に挿入させます。

function downList(obj) {

  // tbody要素に指定したIDを取得し、変数「tbody」に代入
  var tbody = document.getElementById("p2146-tbody");
  // objの親の親のノードを取得し(つまりtr要素)、変数「tr」に代入
  var tr = obj.parentNode.parentNode;

  // もし「tr」の直前の兄弟ノード名が「TR」だった場合
  // (上に「行」が存在している場合)
  if(tr.nextSibling.nodeName === "TR"){
    // 「tr」を直後の兄弟ノードの上に挿入
    tbody.insertBefore(tr.nextSibling, tr);
  }

}

行の一部を変更する

テーブル内の<select>要素のオプションが選択されると、以下のchangeList()関数が呼び出されます。処理内容としては、新たにtd要素を作成し、その要素内を選択されたオプションの値を取得して置き換え、さらに既存のtd要素と置き換える。

function changeList(obj) {

  // 選択したオプションの値を取得し、変数「type」に代入
  var type = obj.value;
  // objの親の親のノードを取得し(つまりtr要素)、変数「tr」に代入
  var tr = obj.parentNode.parentNode;
  // 「tr」の2番目のセルを指定し、変数「td」に代入
  var td = tr.childNodes[1];

  // 新たにtd要素を作成し、変数「cell」に代入
  var cell = document.createElement("td");
  // 「cell」内のHTMLを「type」に置き換え
  cell.innerHTML = type;
  // 「td」を「cell」に置き換え
  tr.replaceChild(cell, td);

}

以上のような感じで実装を試みたわけですが、純粋なJavaScriptのみでDOMを扱うとなると、ブラウザーによる挙動が不確定でわけがわからなくなります。そこでjQueryを使って書き換えてみることにしました。jQueryを使うことによって、以下のようにだいぶシンプルにコードを書くことができます。

jQueryを使って実装

jQueryを使う場合も、上記の純粋なJavaScriptのみで実装する時とほぼ同じような考え方で実装していくことができますが、一箇所だけ大きく異なる点があります。

行を追加する際に、上記ではテーブルの1行目の行を複製して挿入しましたが、今回は処理を簡単にするために、その複製用の1行目の行をCSSで非表示にしています。そのためテーブルを表示させる際に、その非表示とした1行目の行を複製して表示させる処理を追加しています。

HTML

jQueryを使うと、ブラウザーごとのDOMにおける改行の扱いを気にする必要はありません。HTMLも自分の見やすいようにコーディング可能です。今回はイベントハンドラではなく、jQueryで処理をしやすくするためにクラス属性をセットしておきます

<table id="p2146-2-table">

<thead>
<tr>
  <th>リスト選択</th>
  <th>リスト名</th>
  <th>リスト順</th>
  <th>追加/削除</th>
</tr>
</thead>

<tbody id="p2146-2-tbody">
<tr>
  <td>
    <select class="changeList">
      <option>A</option><option>B</option><option>C</option>
    </select>
  </td>
  <td>A</td>
  <td>
    <img src="up.png" alt="↑" class="upList" />上へ 
    <img src="down.png" alt="↓" class="downList" />下へ
  </td>
  <td>
    <input value="+" type="button" class="addList" /> 
    <input value="-" type="button" class="removeList" />
  </td>
</tr>
</tbody>

</table>

CSS

先にも述べたように、1行目の行を複製用のサンプルコードとして非表示にします。

#p2146-2-tbody tr:first-child {
  display: none;
}

jQuery(JavaScript)

ポイントとしては、jQuery 1.7から追加された.on()メソッドを使ってイベントをバインドしています。jQuery 1.7以前であれば.live()メソッドを使うところだと思いますが、この.onメソッドは、その.live()メソッドをはじめとして、.bind()メソッド、.delegate()メソッドの機能が統合された強力な機能を持つメソッドとなっています。

.on()メソッド

$(elements).on( events [, selector] [, data] , handler );
.on() – jQuery API

jQueryのソースコードは以下となります。

<script>
$(document).ready(function () {

  // CSSで非表示にした1行目の行を複製し、その行の下に挿入
  $("#p2146-2-tbody > tr").eq(0).clone(true).insertAfter($("#p2146-2-tbody > tr")).eq(0);

  // 行を追加する
  $(document).on("click", ".addList", function () {
    $("#p2146-2-tbody > tr").eq(0).clone(true).insertAfter(
      $(this).parent().parent();
    );
  });

  // 行を削除する
  $(document).on("click", ".removeList", function () {
    $(this).parent().parent().remove();
  });

  // 行を一つ上に移動させる
  $(document).on("click", "#p2146-2-tbody > tr:gt(1) .upList", function () {
    var t = $(this).parent().parent();
    if(t.prev("tr")) {
      t.insertBefore(t.prev("tr")[0]);
    }
  });

  // 行を一つ下に移動させる
  $(document).on("click", ".downList", function () {
    var t = $(this).parent().parent();
    if(t.next("tr")) {
      t.insertAfter(t.next("tr")[0]);
    }
  });

  // 行の一部を変更する
  $(document).on("change", ".changeList", function () {
    $(this).parent().next().html($(this).val());
  });

});
</script>

< jQueryで実装したサンプル >

上記のサンプルと特に動きは変わらないと思います。

リスト選択リスト名リスト順追加/削除
A 上へ  下へ 

以上のように、tableの行を操作する機能について紹介しましたが、この機能の使い道として考えられることとしては、やはりajax関連の機能ではないでしょうか。最初にも書きましたが、現在このブログに新しい機能を実装しようと考えています。

その機能というのが、簡単に言えば「後で読む」機能で、トップページの記事一覧にそれぞれ「後で読む」ボタンを設置しておき、そのボタンを押すと、サイドバーにリンクつきタイトルが追加されるといったものです。タイトルをクリックして記事にアクセスするとサイドバーの表示から消え、さらに順番も変更できるといったものです。これをHTML5のweb storageを使って実装できればなと思っています。

Web制作の現場で使うjQueryデザイン入門[改訂新版](ドーナッツ本)
  • 『Web制作の現場で使うjQueryデザイン入門[改訂新版]』(通称: ドーナツ本)
  • 著者: 西畑一馬
  • 出版社: KADOKAWA/アスキー・メディアワークス
  • 単行本: 312ページ
  • 発売日: 2013年3月7日
  • ISBN: 4048913913

スポンサードリンク

コメント一覧

  1. 大変参考になりました。ありがとうございます。
    実際に使用する上で気をつけるのは
    テーブルにはじめから複数行ある場合、ダミー行が複数個コピーされてしまうので

    $("#p2146-2-tbody>tr").eq(0).clone(true).insertAfter($("#p2146-2-tbody>tr").eq(0));

    とすること、
    parent()を使っていることでボタンの親にtd,trという構造が前提になっていることなどでしょうか。

    これらの機能をまとめてjqueryプラグイン化して
    $(‘#table’).rowMovable()のように一発で使えるようにすると楽でしょうね。

    tsgr  返信

    • tsgrさん

      コメントありがとうございます。

      > テーブルにはじめから複数行ある場合、ダミー行が複数個コピーされてしまうので

      こちら仰る通りですね。サンプルの方も.eq(0)を最後に追加して1行目のみコピーするようにしました。

      > これらの機能をまとめてjqueryプラグイン化して$(‘#table’).rowMovable()のように一発で使えるようにすると楽でしょうね。

      要望があれば、プラグイン化もやってみたいですね。その場合は、ご指摘のparent()を使っている部分も違う方法で実装することになりますね。

      今後ともよろしくお願いいたします。

      Takanori Maeda  返信

  2. 大変参考になりました。
    empty()remove()に変更するなどして利用させていただきました。
    ありがとうございます。

    田中  返信

    • 田中さん

      コメントありがとうございます。確かにempty()だと親要素が残ったままになってしまいますね。こちらのサンプルもremove()に変更しました。

      今後ともよろしくお願いいたします。

      Takanori Maeda  返信

  3. select を含んだ行の追加方法をいろいろ探しましたが、このページが一番わかりやすかったです。
    大変助かりました。
    ありがとうございました。

    js初心者  返信

    • js初心者さん

      コメントありがとうございます。JSの実装うまくいけたでしょうか。わかりやすいと言っていただけて大変嬉しいです。今後ともよろしくお願いいたします。

      Takanori Maeda  返信

  4. Jqueryでテーブルの行の追加、削除などができる方法を探していてこちらのサイトにたどり着きました。
    わかり易いサイトで参考にさせていただいております。
    ありがとうございます。

    よろしければ教えていただきたいのですが、テキストボックスや、セレクトボックスなどを配置したテーブルを考えています。
    CGIなどで値を取得したいためです。

    なので、行を追加した時、削除した時に、それぞれ固有の値として取得したいので、1行ずつテキストボックスなどのnameに、新しくナンバーを振ったり、削除した分を振り直したりする機能がほしいのですが、それはどのようにしたらいいのでしょうか。
    現在、いろいろと試しているのですが、上手く行かず、教えていただけましたら幸いです。
    よろしくお願いいたします。

    solt  返信

    • soltさん

      コメントありがとうございます。

      > なので、行を追加した時、削除した時に、それぞれ固有の値として取得したいので、1行ずつテキストボックスなどのnameに、新しくナンバーを振ったり、削除した分を振り直したりする機能がほしいのですが、それはどのようにしたらいいのでしょうか。

      ナンバーの振り直しなども考慮に入れると、例えば以下のような関数を作って、

      function addNumber() {
        var td = $("#p2146-2-tbody > tr").find("input[type=text]");
        td.each(function (i) {
          $(this).attr("name",  "number_" + i);
        });
      }

      行の追加時と削除時に実行してあげるような方法はどうでしょうかね。

      // 行を追加する
      $(document).on("click", ".addList", function() {
        $("#p2146-2-tbody > tr").eq(0).clone(true).insertAfter(
          $(this).parent().parent()
        );
        addNumber(); // ←追加
      });
      // 行を削除する
      $(document).on("click", ".removeList", function() {
        $(this).parent().parent().remove();
        addNumber(); // ←追加
      });

      ただし、行を追加/削除する度に全部の行に対して処理が走るので、行数が増えた時のパフォーマンスに影響を与えそうです。

      参考にしてみてください。
      ご不明点あればご遠慮なく仰ってください。

      Takanori Maeda  返信

  5. 行の削除の時、最後の1行は残したままにするにはどうしたらいいですか?
    全部消えてしまうと困るので

    tsio  返信

    • tsioさん

      コメントありがとうございます。

      > 行の削除の時、最後の1行は残したままにするにはどうしたらいいですか?
      > 全部消えてしまうと困るので

      そうですね。その場合は、削除ボタンを押した際に、tbody要素の中のtrの数を数えて、2以下(clone用に非表示にしているtrと最後の1行のtr)であれば処理を終了するようにしてあげると良いですね。

      jQueryで書くと以下のようになります。

      $(document).on("click",".removeList",function () {
        if ($("#p2146-2-tbody > tr").length <= 2) {
          return;
        }
        $(this).parent().parent().remove();
      });
      

      また、この時削除ボタンを押せないようにしてあげると、UX的にもよくなります。ただし、追加ボタンを押した際に、同様にtrの数を数えて、削除ボタンを押せるようにする処理が必要になってきます。

      参考にしてみてください。
      ご不明点あればご遠慮なく仰ってください。

      Takanori Maeda  返信

      • なるほど.lengthで要素数が取得できるのですね。
        丁寧な解説ありがとうございます

        tsio  返信

        • tsioさん

          無事、解決されましたでしょうか。他にも何かあれば、ご遠慮なく仰ってください。

          今後ともよろしくお願いいたします!

          Takanori Maeda  返信

  6. 大変参考になりました。
    良記事のご投稿ありがとうございます。

    ご多忙の所申し訳ありませんが、1点アドバイス頂きたいです。以下のソースコードで、tbody id=”p2146-2-tbody” ~ /tbodyの範囲を追加、削除、移動、変更させたい場合は、どのようにすべきでしょうか?
    現在、上手く動かせず、教えて頂けましたら幸いです。宜しくお願い致します。

    <table id="p2146-2-table">
    <tbody id="p2146-2-tbody">
    <tr>
      <th> リスト選択 </th>
      <th> リスト名 </th>
      <th> リスト順 </th>
    </tr>
    
    <tr>
      <td>
        <select class="changeList">
          <option> A </option><option> B </option><option> C </option>
        </select>
      </td>
      <td> A </td>
      <td>
        <img src="up.png" alt="↑" class="upList" 上へ >
        <img src="down.png" alt="↓" class="downList" 下へ>
      </td>
    </tr>
    
    <tr>
      <th> test1 </th>
      <th> test2 </th>
      <th> test3 </th>
    </tr>
    
    <tr>
      <td>
        <select class="changeList">
        <option> A </option><option> B </option> <option> C </option>
        </select>
      </td>
      <td> A </td>
      <td>
        <img src="up.png" alt="↑" class="upList" 上へ> 
        <img src="down.png" alt="↓" class="downList" 下へ>
      </td>
    <tr>
    
    <tr>
      <td>
       <input value="+" type="button" class="addList" >
       <input value="-" type="button" class="removeList">
      </td>
    </tr>
    
    </tbody>
    </table>
    

    何度もコメントしてしまい申し訳ありませんm(_ _)m

    Hagiwara  返信

    • Hagiwaraさん

      コメントありがとうございます。

      > 以下のソースコードで、tbody id=”p2146-2-tbody” ~ /tbodyの範囲を追加、削除、移動、変更させたい場合は、どのようにすべきでしょうか?

      「tr要素のグループごと」ってことですね。tbody要素はtable要素内で複数指定することができるので、それぞれのtr要素のグループをtbody要素でラップして、tbody要素を対象に追加、削除、移動、変更をさせてあげると良いでしょうかね。

      記事内のソースコードの以下の部分を変更します。
      $(this).parent().parent()

      サンプルではtr要素を対象に動かすようになっていましたが、さらに上の階層のtbody要素を対象にするので、以下のようにしたらどうでしょう。
      $(this).parent().parent().parent()

      参考にしてみてください。
      ご不明点あればご遠慮なく仰ってください。

      Takanori Maeda  返信

  7. 追加した行を「保存」できないでしょうか。

    ブラウザの更新や他ページから戻ってくる際にクリアされてしまうためこれを解決したいです。

    localStorageで調べたのですが知識が追い付かず分かりませんでした。

    お手数ですがご教授くだされば幸いです。どうぞよろしくお願いいたします。

    takita  返信

  8. 度々すみません 追記します。
    追加した行毎の「保存」が困難な場合、追加した行をまとめて「保存」ができればいいのですが、いかがでしょうか。

    takita  返信

    • takitaさん

      コメントありがとうございます。

      > 追加した行毎の「保存」が困難な場合、追加した行をまとめて「保存」ができればいいのですが、いかがでしょうか。

      この記事で紹介した方法で追加したDOMを保存しておきたい場合は、table全体をlocalStorageなりで保存するのが良いかと思います。

      table全体を保存する処理を書いておき、行を追加、削除、変更する度に、その処理を呼び出すようにします。

      ページを開いた際に保存先を確認し、保存されていれば、それを表示、されていなければデフォルトを表示させるといった感じでしょうか。

      自分は最近はよくReactを使っていますが、Reactだと行の状態を配列に持たせて、その配列を保存させておくといった方法も可能ですね。

      イメージはつきましたでしょうか。ご不明な点があれば、ご遠慮なく仰ってください。

      Takanori Maeda  返信

  • 必須

コメント