一括削除の機能を追加する。

以前、softwarebookプロジェクトでやったように(日記2/14〜2/18頃)、一括削除の機能を追加してみた。今回は、実現する機能は同じだけど、もう少し効率的な方法で処理したい。

まずは削除対象を選択するチェックボックスの追加から。

app/views/csvs/_listd.rhtml
ビュー(リストデータの1行を描画する部分)
  • 削除リンクをコメントアウトで取り除いて、チェックボックスを追加した。
  • そのチェックボックスをobserve_fieldで監視して、クリックされたらchecked_updateアクションを実行、チェックされた行の強調表示をon/offを処理する。
      • 以前はここでlist_updateアクションを実行して、リストデータ全体を再描画していた...。(1行の更新のために、全体を再描画、なんと無駄の多いことか。)
  • チェックボックスで選択項目を取り出したい場合は、check_box_tag "checked_csvs[]", listd.idのように設定しておくのが良さそうだ。(name属性とvalue属性の組合せは、このように利用することを前提に設計されているらしい。後々、JavaScriptで処理する時も、この方が扱い易い。)
    • パラメータparams[:checked_csvs]は、["1", "2", "3"]のように、配列で取得できる。
      • 以前はcheck_box_tag "checked_csvs[#{listd.id}]"のように指定していた。(パラメーターparams[:checked_csvs]は、{"1"=>"1", "2"=>"1", "3"=>"1"}のようにハッシュで取得出来る。ハッシュの方が存在の有無を簡単に確認出来そうだったので、このようにしていた。)
...(途中省略)...
      <%#= link_to_remote_if management_section?(listd.management_section), '削除', 
                     :update=>'list_update', 
                     :submit=>'list_params', 
                     :url=>{:action=>'destroy', :id=>listd}, 
                     :confirm=>_('Are you sure?'), :post=>true, 
                     :loading=>visual_effect(:drop_out, "csv_#{listd.id}") %></td>
      <%= check_box_tag "checked_csvs[]", listd.id, nil, 
                        :disabled=>!management_section?(listd.management_section), 
                        :id=>"checked_csvs_#{listd.id}" %>
      <%= observe_field "checked_csvs_#{listd.id}", 
                        :submit=>'list_params', 
                        :url=>{:action=>'checked_update', :id=>listd.id} %>
...(途中省略)...
app/controllers/csvs_controller.rb
コントローラー
  • 最初は、_listd.rhtmlを実行して、1行全部を再描画していた。
  • その後、もう少しJavaScriptの領域に踏み込めば、クラス名checkedだけを追加/削除できることが分かり、以下のようになった。
  • チェックボックスの状態に応じて、クラス名checkedを追加、または削除する。これによって行の背景色が変化する。
    • Element.addClassName、Element.removeClassNameによって、クラス名を追加/削除するメソッド。
class CsvsController < ApplicationController
...(途中省略)...
  def checked_update
    render :update do |p|
      p << <<-END
        if ($('checked_csvs_#{params[:id]}').checked) {
          Element.addClassName('csv_#{params[:id]}','checked');
        }
        else {
          Element.removeClassName('csv_#{params[:id]}','checked');
        }
      END
    end
  end
...(途中省略)...

つぎに一括削除ボタンを作る。

app/views/csvs/list.rhtml
ビュー(リストページ全体を描画する部分)
      <%= submit_to_remote :csv, 'チェックした項目を削除', 
                           :submit=>'list_params', 
                           :url=>{:action=>'destroy_all'},
                           :confirm=>_('Are you sure?') %> 
app/controllers/csvs_controller.rb
コントローラー
  1. 削除対象が選択されているかチェックする。
  2. params[:checked_csvs]のid配列が指定する、データベースのレコードを削除する。
  3. ビジュアルエフェクトで削除対象の行を消す。
  4. リスト全体を再描画して削除処理が完了する。
  def destroy_all
    render :update do |p|
      if params[:checked_csvs].blank?
        p.alert("削除対象が選択されていません。チェックマークで指定してください。")
      else
        Csv.destroy(params[:checked_csvs])
        params[:checked_csvs].each{|k| p.visual_effect(:drop_out, "csv_#{k}")}
        p.delay(2) do
          p.replace_html :list_update, render_component(:action=>'list_update', :params=>params)
        end
      end
    end
  end

すべて選択/解除のリンクも作る。

フィルターで絞り込んだ検索結果をすべて削除したいこともあるはず。そんなときチェックボックスを一つひとつクリックするのは面倒な作業だ。リストすべてにチェックを入れたり、外したり出来るリンクも作ってみた。

app/views/csvs/list.rhtml
ビュー(リスト全体を描画する部分)
  • 処理はパラメーター無しで、check_all、uncheck_allアクションに任せている。
      <small>
        すべて
        <%= link_to_remote '選択', :url=>{:action=>'check_all'} %> |
        <%= link_to_remote '解除', :url=>{:action=>'uncheck_all'} %>
      </small>
app/controllers/csvs_controller.rb
コントローラー
  • ここでは、render :updateブロック内で、JavaScriptを直接実行している。チェックボックスが編集可能状態化を判定して、以下の処理を行っている。
  • <<-ENDからENDまではヒアドキュメント。""で囲った文字列の扱いと同じ。
  • 以下のJavaScriptは、user_engineのチェックボックスに一括でチェックを入れるコードを参考にした。(vender/plugins/user_engine/public/javascripts/user_engine.js)
      • 以前は、リスト全体を再描画してチェックを入れていた。今回は、描画せずに、checked属性とclass属性を追加するだけなので、処理速度は以前と比べて相当な差がある。リンクをクリックすれば一瞬ですべてが選択状態に。以前はチェックされている様子がダラダラと見えていた...。
class CsvsController < ApplicationController
...(途中省略)...
  # すべてのチェックを入れて、クラス名checkedを追加、行の背景色がグレイに変化する。
  def check_all
    render :update do |p|
      p << <<-END
        boxes = document.getElementsByName('checked_csvs[]');
        for(i = 0; i < boxes.length; i++){
          if (!boxes[i].disabled){
            boxes[i].checked = true;
            Element.addClassName(boxes[i].parentNode.parentNode, 'checked');
          }
        }
      END
    end
  end
  
  # すべてのチェックを外して、クラス名checkedを削除、行の背景色が元に戻る。
  def uncheck_all
    render :update do |p|
      p << <<-END
        boxes = document.getElementsByName('checked_csvs[]');
        for(i = 0; i < boxes.length; i++){
          if (!boxes[i].disabled){
            boxes[i].checked = false;
            Element.removeClassName(boxes[i].parentNode.parentNode, 'checked');
          }
        }
      END
    end    
  end

observe_fieldに反応しないイベント?

ここまで実装して、ちょっとした問題が発生。すべて選択/解除を実行した直後、チェックボックスをクリックしても、背景色が変化しないのだ...。どうも、すべて選択/解除を実行した直後は、何故かobserve_fieldがイベントを拾ってくれないみたい。次のクリックイベントからは正常に反応している。謎だ...。
上記原因は分からないままだが、解決策は見つかった。observe_fieldは削除して、:onclikオプションを使って以下のようにやってみた。これで、すべて選択/解除を実行した直後も、正常に動作するようになった。

app/views/csvs/_listd.rhtml
ビュー(リストデータの1行を描画する部分)
      <%= check_box_tag "checked_csvs[]", listd.id, nil, 
                        :disabled=>!management_section?(listd.management_section), 
                        :id=>"checked_csvs_#{listd.id}", 
                        :onclick=>remote_function(:url=>{:action=>'checked_update', :id=>listd.id}) %>


以上で、ひとまず一括削除ができるようになった!無駄な処理を無くすように心掛けたので、以前より操作反応は良いと思う。ページ更新を効率的に処理するためには、JavaScriptの知識も必要だと実感した。