改良中...明細行を挿入、削除、コピーしたい!
伝票入力していると、ユーザーは必ず明細行の挿入、削除、コピーをしたくなるものだ。ほとんど内容が同じ行を繰り返し入力する気にはなれないし、入力忘れがあったらその位置に挿入したくなるし、不要な行は削除したくなる。自然な要望だ。そもそもコンピューターを使えば挿入、削除、コピーが簡単にできることがウリのはず。
ところが、いざその機能を実装しようとすると、結構手強い。既にデータベースに登録されたデータを扱う場合には、Railsにはacts_as_listという拡張機能がある。それが利用できれば手軽に実現できそうなのだ。しかし、今回は以下のような状況で、果たして利用できるのか?
- 伝票入力中のデータはまだデータベスに保存されていない。
- 編集中の伝票には、未入力の行もある。未入力の行もその状態を保持して、挿入、削除、コピーできるようにしたい。
- そしてデータベースに保存する時は、入力されている行だけ保存する。未入力の行は保存しない。
自分なりにやってみたが、acts_as_listの利用は諦めた...。(idが設定されていない状況で使う方法ってあるのだろうか?)journalモデル、slipモデルとslipsコントローラーにメソッドを追加して、以下のようにやってみた。
index値を保持する
現状では、ページが更新される度に明細行(journalインスタンス)のindex値が変わってしまう。挿入、削除、コピーを実現するためには、一度設定されたindex値は、入力中は保持しておく必要がある。(...と思う。indexがidの代わりになる。)
- indexがnilの時だけ、新規indexを設定するように変更した。
# モデル: app/models/journal.rb class Journal < ActiveRecord::Base ...(中略)... def initialize(*attr) super # 以下の形式でユニークなインデックス値を設定する。 # 例: j948696358 @index ||= "j#{Time.now.hash.abs}" end def index id || @index end
- hidden_fieldにより、常にindex値をパラメーターとして送信することにした。(すると、ビューとコントローラーの間でindex値のキャッチボールを繰り返すことになり、その結果、index値が保持されることになる。)
<%# ビュー: app/views/journals/_form.rhtml %> ...(中略)... <%= hidden_field 'journal', 'index', :index=>@journal.index %> ...(中略)...
挿入、削除、コピーのリンクを作る
- 明細行の_form.rhtmlには、挿入(+)・削除(ー)・コピー(▼)のリンクも追加して、以下のようになった。
- :submit=>'slip'は、この伝票全体の情報を送信することになる。(無駄が多いので、あとで調整するかもしれない。)
- :title属性を設定しておくと、マウスがリンク上にある時、ツールチップとして文字列が表示される。(対応しているブラウザで)
<%# ビュー: app/views/journals/_form.rhtml %> <% @journal = form %> <% @journal_count += 1 rescue @journal_count = 1 %> <!--[form:journal]--> <tr valign="top" id="<%= @journal.index %>"> <th> <%= link_to_remote "+", {:submit=>'slip', :url=>{:action=>'insert_row', :index=>@journal.index}}, {:title=>"1行挿入"} %> <%= link_to_remote "ー", {:submit=>'slip', :url=>{:action=>'delete_row', :index=>@journal.index}}, {:title=>"1行削除"} %> <%= link_to_remote "▼", {:submit=>'slip', :url=>{:action=>'copy_row', :index=>@journal.index}}, {:title=>"1行コピー"} %> </th> <th align="right"> <%#= text_field 'journal', 'position', :index=>@journal.index, :value=>@journal_count, :size=>4, :readonly=>true %> <%= @journal_count %> <%= hidden_field 'journal', 'position', :index=>@journal.index, :value=>@journal_count %> <%= hidden_field 'journal', 'index', :index=>@journal.index %> </th> <td align="_center"> <%= text_field 'journal', 'comment', :index=>@journal.index, :size=>40 %> <%= error_messages_on 'journal', 'comment' %> </td> <td align="_center"> <%= yen_field 'journal', 'yen', :index=>@journal.index %> <%= error_messages_on 'journal', 'yen' %> </td> </tr> <!--[eoform:journal]-->
- 明細の列タイトルにも空の1列を追加しておく。(ないと表示がズレてしまうので。)
<%# ビュー: app/views/journals/_header.rhtml %> <!--[header:journal]--> <tr> <th></th> <th>No.</th> <th>摘要</th> <th>金額</th> </tr> <!--[eoheader:journal]-->
slipsコントローラーで処理する
挿入+、削除ー、コピー▼のリンクをクリックした時のパラメーターを受け取って、slipsコントローラーでは以下のように処理する。
- 受け取ったパラメーターから、伝票、明細行の状態を復元(before_filterで、for_editing_rowメソッドを呼び出している。)
- Slipモデルで、実際の挿入・削除・コピーの処理を行う。
- render :updateで明細行の部分(journals_form)だけ再描画する。
# コントローラー: app/controllers/slips_controller.rb class SlipsController < ApplicationController before_filter :for_editing_row, :only=>[:insert_row, :copy_row, :delete_row] ...(中略)... private def for_editing_row @slip = Slip.new(params[:slip]) @slip.make_journals(params[:journal]) end public def insert_row @slip.insert_journal(params[:id]) render :update do |page| page.replace_html 'journals_form', :partial=>'journals/form', :collection=>@slip.editing_journals end end def copy_row @slip.copy_journal(params[:id]) render :update do |page| page.replace_html 'journals_form', :partial=>'journals/form', :collection=>@slip.editing_journals end end def delete_row @slip.delete_journal(params[:id]) render :update do |page| page.replace_html 'journals_form', :partial=>'journals/form', :collection=>@slip.editing_journals end end end
- 再描画する部分のid属性 journals_form は、<tbody id="journals_form">タグで囲った以下の範囲。
<%# ビュー: app/views/slips/_form.rhtml %> <!--[form:slip]--> ...(中略)... <table class="journal"> <%= render :partial=>'journals/header' %> <tbody id="journals_form"> <%= render :partial=>'journals/form', :collection=>@journals %> </tbody> </table> <!--[eoform:slip]-->
slipモデルで処理する
- コントローラーから呼び出され、実際に挿入・削除・コピーを処理するコードは以下のようにしてみた。
# モデル: app/models/slip.rb ...(中略)... # index値で指定されたJournalインスタンスを返す def journal_at(index) @editing_journals.find {|item| item.index == index} end def copy_journal(params_index) current_journal = journal_at(params_index) insert_index = current_journal.position insert_journal = Journal.new(current_journal.attributes) @editing_journals.insert(insert_index, insert_journal) end def insert_journal(params_index) current_journal = journal_at(params_index) insert_index = current_journal.position - 1 insert_journal = Journal.new @editing_journals.insert(insert_index, insert_journal) end def delete_journal(params_index) current_journal = journal_at(params_index) delete_index = current_journal.position - 1 @editing_journals.delete_at(delete_index) end
- 上記コードで、Journal.new(current_journal.attributes)を実行した時に、'yen' => nilや数値 の場合があり、エラーになってしまったので、文字列の場合だけ処理するようにした。
# モデル: app/models/slip.rb class Journal < ActiveRecord::Base ...(中略)... def yen=(value) # nilや数値の場合エラーが発生するので、文字列だけ処理するようにした。 # value.to_s.gsub(/,/, '')でも良いと思う。 self[:yen] = (value.class == String ? value.gsub(/,/, '') : value) end
javascript_include_tag :defaultsを追記
- そうそう、いつもながら肝心なことを忘れていた。JavaScriptを使うなら、ライブラリを利用できるようにしとかないと...。<head>タグの最後に、以下のように追記した。
# レイアウト: app/views/layouts/slip.rhtml ...(中略)... <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> <title>Slips: <%= controller.action_name %></title> <%= stylesheet_link_tag 'scaffold' %> <%= javascript_include_tag :defaults %> </head> ...(中略)...
以上の変更をして、試してみる。追加+、削除ー、コピー▼のリンクをクリックすると、その動作がちゃんと処理される!出来た...と言いたいところだが、ちょっと不満足なところがある。挿入、削除、コピーの動作が地味すぎて何が起こったのか分かりにくいのだ...。もうちょっと視覚的に分かり易いフィードバックが必要だ。
ビジュアルエフェクトでフィードバックする
そんな時のためにビジュアルエフェクトがある。ここでは以下のようにhighlightを使ってみた。
- copy_jurnal、insert_journal、delete_journalはコピー、挿入、削除したアイテムを返すようにした。(修正箇所はオレンジ)
# モデル: app/models/slip.rb ...(中略)... # index値で指定されたJournalインスタンスを返す def journal_at(index) @editing_journals.find {|item| item.index == index} end def copy_journal(params_index) current_journal = journal_at(params_index) insert_index = current_journal.position insert_journal = Journal.new(current_journal.attributes) @editing_journals.insert(insert_index, insert_journal) insert_journal end def insert_journal(params_index) current_journal = journal_at(params_index) insert_index = current_journal.position - 1 insert_journal = Journal.new @editing_journals.insert(insert_index, insert_journal) insert_journal end def delete_journal(params_index) current_journal = journal_at(params_index) delete_index = current_journal.position - 1 @editing_journals.delete_at(delete_index) # delete_atは削除したアイテムを返してくれる。 end
- モデルでコピー、挿入、削除したアイテムに対し、ビジュアルエフェクトを実行する。(修正箇所はオレンジ)
# コントローラー: app/controllers/slips_controller.rb class SlipsController < ApplicationController before_filter :for_editing_row, :only=>[:insert_row, :copy_row, :delete_row] ...(中略)... private def for_editing_row @slip = Slip.new(params[:slip]) @slip.make_journals(params[:journal]) end public def insert_row @effect_item = @slip.insert_journal(params[:id]) render :update do |page| page.replace_html 'journals_form', :partial=>'journals/form', :collection=>@slip.editing_journals page.visual_effect :highlight, @effect_item.index, :duration=>2 end end def copy_row @effect_item = @slip.copy_journal(params[:id]) render :update do |page| page.replace_html 'journals_form', :partial=>'journals/form', :collection=>@slip.editing_journals page.visual_effect :highlight, @effect_item.index, :duration=>2 end end def delete_row @effect_item = @slip.delete_journal(params[:id]) render :update do |page| page.visual_effect(:highlight, @effect_item.index, :duration=>2, :startcolor=>"'#666666'") page.delay(1) do page.replace_html 'journals_form', :partial=>'journals/form', :collection=>@slip.editing_journals end end end end
以上で試してみる。しかし...ビジュアルエフェクトは機能しているが、あまり目立たない...。問題は、ハイライトで発光する部分が、id属性で指定したタグの背景色に限定されているからのようだ。(この場合、明細行trタグにはセルやフォームがキッチリ配置されていて、背景がほとんど見えない状態なのだ。)
JavaScriptで行全体を発光するフィードバックにする
それなら、td、th、input、textarea、selectタグの背景も発光するように指定すれば良さそう。しかし、Railsだけで制御しようとすると、すべてのタグをid属性で管理する方法しか思いつかない。それはちょっと面倒くさい...。悩んでいると、prototype.jsには、$$("セレクタ")メソッドがあることに気付く。cssでセレクタを指定するように、$$('#j1234 input')とすれば、id属性"j1234"内のinputダグの配列を取得することが出来る。それを利用してみることにする。
- コントローラー内にpage << "JavaScript"で書いてしまうとコードの見通しが悪くなるので、ヘルパーメソッドhighlight_rowとして定義してみた。
- $$('セレクタ')メソッドで取得した配列を、ruby側に渡す方法が分からなかったので、すべてJavaScriptでの処理になった。
# ヘルパ: app/helpers/slips_helper.rb module SlipsHelper def highlight_row(page, id, js_options={}) page << "cells = $$('##{id} th')" page << "cells = cells.concat($$('##{id} td'))" page << "cells = cells.concat($$('##{id} input'))" page << "cells = cells.concat($$('##{id} textarea'))" page << "cells = cells.concat($$('##{id} select'))" page << "for (i = 0; i < cells.length; i++)" page << "new Effect.Highlight(cells[i], #{options_for_javascript(js_options)})" end end
上記JavaScriptは、Rubyと融合させて、以下のように書けることがわかった!
module SlipsHelper # 行全体をハイライト処理する def highlight_row(page, id, js_options={}) tags = %w{th td input textarea select} tags.each do |tag| page.select("##{id} #{tag}").each {|element| page.visual_effect(:highlight, element, js_options)} end end end
- visual_effect :highlightは、すべてhighlight_rowに置き換えた。
- ついでに、insert_rowとcopy_rowのrender :update部分が重複しているので、render_insertメソッドを定義してみた。
# コントローラー: app/controllers/slips_controller.rb class SlipsController < ApplicationController before_filter :for_editing_row, :only=>[:insert_row, :copy_row, :delete_row] ...(中略)... private def for_editing_row @slip = Slip.new(params[:slip]) @slip.make_journals(params[:journal]) end def render_insert render :update do |page| page.replace_html 'journals_form', :partial=>'journals/form', :collection=>@slip.editing_journals highlight_row(page, @effect_item.index, :duration=>2) end end public def insert_row @effect_item = @slip.insert_journal(params[:id]) render_insert end def copy_row @effect_item = @slip.copy_journal(params[:id]) render_insert end def delete_row @effect_item = @slip.delete_journal(params[:id]) render :update do |page| highlight_row(page, @effect_item.index, :duration=>2, :startcolor=>"'#666666'") page.delay(1) do page.replace_html 'journals_form', :partial=>'journals/form', :collection=>@slip.editing_journals end end end end
以上で、実行してみる。行全体が発光するようになり、追加、削除、コピーの動きがよく分かるようになった!これなら満足。