todo を眺める その 2 (CUD な処理の詳細について)

最新 LL フレームワークエクスプローラに出ていた todo アプリケーションの更新処理の詳細について。

list の追加

以下に view の一部を引用

<%= form_remote_tag :url => {:controller => 'ajax', :action => 'add_list'} ,
 :update => { :success => 'tasklists' },
 :failure => 'alert(request.responseText)',
 :position => :bottom ,
 :html => { :class => 'add_list' }
 %>

form_remote_tag を使用している、という事で XMLHttpRequest を使用してフォームデータの送信を行なう。以下にパラメータについてまとめておく。ちなみに form_remote_tag は rails/actionpack/lib/action_view/helpers/javascript_helper.rb にて定義、となっている (はず)。

  • url パラメータにはフォームデータを受け取るアクションを指定。ここでは、task コントローラの index アクションで表示されたフォームから送信されるため、コントローラの指定の必要に違いない、と勝手に類推。
  • update パラメータには DOM エレメントの ID 属性を指定との事。app/views/task/index.rhtml の末端部分に TODO リストの明細行を表示する部分が記述されている。以下。
<div id='tasklists'>
<%= render :partial => 'list', :collection => @lists %>
</div>

更新が成功した場合には、tasklist な ID を持つ明細行が更新される。

  • failure パラメータではアクション実行中に何らかのエラーがあった場合に、という事かなぁ。英語のドキュメント含め、微妙。ソースもナニ (弱)。あと、request.responseText というのも何だかワケワカ。調査必要ッス。
  • position パラメータはアクションにて render された情報を ID に挿入する位置の指定となる、との事。この場合には末端に追加、となる。
  • html パラメータも微妙。CSS のための class を指定しているだけ、と見たい。(こら

で、対応するアクションが add_list#ajax_controller.rb という事で一連の処理を以下に引用。

 def add_list
   list = List.new(:title => params[:title])
   render_add(list,'list')
 end

 protected

 def render_add(ar,name,partial_tempate_name = name)
   if ar.save
     render(:partial => "task/#{partial_tempate_name}" ,
            :status => 200 ,:locals => {name.to_sym => ar})
   else
     render :text => ar.errors.full_messages.join("\n") ,:status => 406
   end
 end

ajax コントローラの全体を見てみると分るのですが、render_add とか render_destroy とかのメソドが DRY なナニを意識されているようで格好良し。
やっている事自体は List なオブジェクトを new して save の結果によって render の処理を分けております。しかし ar.save が成功したトキの render のナニはなんとゆーか格好ええのぅ。add_list アクションの場合は、以下のような形に展開されるはず。

render(:partial => "task/list", :status => 200, :locals => { :list => ar })

list が item に置き換える事が可能なあたりに ruby なナニが (以下略

で、返却された render なソレを view の方で末端に追加、という事ですか。

list の削除が可能

list 削除な view (app/views/task/_list.rhtml) を以下に引用。

<%= link_to_remote 'DELETE',
    {
    :url => {:controller => 'ajax', :action => 'destroy_list', :id => list.id },

    :success => "Element.remove($('tasklist_#{list.id}').parentNode)",
    :confirm => "『#{list.title}』を削除します。\nよろしいですか?"
    },
    :class => 'menu_link'
     %>

link_to_remote は rails/actionpack/lib/action_view/helpers/prototype_helper.rb にて定義されている。コメントがとても親切というか分かりやすい。

  • 最初のパラメータ文字列はハイパーリンク文字列
  • 次のハッシュはメソド定義における options パラメータになる。
    • url パラメータはリンクのクリック時に起動されるアクションとなる。ajax コントローラの destroy_list アクションに list.id を渡す、という意味。
    • success パラメータはコールバックの指定。ソースのコメントによると_Called when the XMLHttpRequest is completed, and the HTTP status code is in the 2XX range._との事。コールバックとして Element.remove が呼ばれる。script.aculo.us 製 (prototype.js?? あ、This page is part of the Prototype JavaScript framework documentation. って書いてる) らしい。ドキュメントによると_removes the element from the DOM tree._とある。ふむふむ。
      ちなみに Element.remove で何が remove されるのか、も興味深い。
  • で、最後の class パラメータは CSS 用という事にしておきます。(弱

# 追補
# $() は prototype.js で用意されているショートカットらしい。
# http://www.imgsrc.co.jp/~kuriyama/prototype/prototype.js.html#DollarFunction

対応するアクションは destroy_list#ajax_controller.rb との事にて関係あるナニを以下に引用。

  public
  def destroy_list
    render_destroy List.find(params[:id])
  end

  protected

  def render_destroy(ar)
    ar.destroy
    render :nothing => true # 何も表示しない
  end

ここでも render_destroy は DRY なナニで item 削除時に流用可能な形になっている。又、view 側で Element.remove しているので render は何もしなくて良い、と。一応 render はしないとイカンのかなぁ。別途コメントアウトなアレで動作確認してみたいかな、と。

item の追加

item 追加に関連する view (app/views/task/_list.rhtml) の一部を以下に引用。

<%= link_to_function 'ADD', "Element.toggle('add_item_#{ list.id }')", :class => 'menu_link' %>

<%= form_remote_tag :url => {:controller => 'ajax', :action => 'add_item', :id =
> list.id },
    :failure => 'alert(request.responseText)',
    :complete => "Element.hide('add_item_#{ list.id }')",
    :position => :bottom ,
    :update => { :success => "list_#{list.id}" } ,
    :html => {:style => 'display:none ;', :id => "add_item_#{list.id}" }
     %>
<%= text_field_tag 'note', nil, :id => "note_#{list.id}" %>
<br />
<%= submit_tag 'ADD' %>
<%= link_to_function 'cancel', "Element.hide('add_item_#{ list.id }')" %>
<%= end_form_tag %>

ここでは ajax 的な多段構成になっている。
まず、link_to_function で ADD というリンクを作成している。link_to_function は rails/actionpack/lib/action_view/helpers/javascript_helper.rb にて定義。http://railsapi.masuidrive.jp/module/ActionView::Helpers::JavaScriptHelper/link_to_function によると、_Returns a link that’ll trigger a JavaScript function using the onclick handler and return false after the fact._との事。このリンクがクリックされないと、Element.toggle で指定されている ID は表示されない、と見てビンゴでしょうか。
又、list の delete と見栄えを統一するために、delete と同じ class が指定されている。(類推

で、 ADD なリンクがクリックされたら表示されるパーツが上記の form_remote_tag な部分 (end_form_tag マデ)。form_remote_tag の html オプションにて id が指定されており、link_to_function のナニと同一になるのが分かる。詳細なツッコミは略すが、ざっくりなナニを以下に列挙。

  • 対応するアクションは add_item#ajax_controller.rb。Item.new して render_add を呼び出しているだけなので詳細は略。
  • アクションが正常終了したら、フォームを Element.hide で閉じる
  • データは item リストの末端に追加
  • style オプションの 'display:none;' は要調査

あと、form なヘルパーメソドのフォローを以下に。共に定義は rails/actionpack/lib/action_view/helpers/form_tag_helper.rb な模様。

又、cancel なナニは link_to_function を使っている。正常終了のケースと同様にElement.hide でパーツを閉じているのが分かる。

item の更新、削除

item の更新や削除に関する view は app/views/task/_item.rhtml となっている。一部を以下に引用。

<span class='item_edit_area' id='item_edit_area_<%= item.id %>'><%= h item.note 
%></span>
</li>
<script type="text/javascript">
new Ajax.InPlaceEditor(
  'item_edit_area_<%= item.id %>',
  '<%= url_for :controller => 'ajax', :action => 'edit_item', :id => item.id %>'
,
  {
    onComplete: function(transport, element) {
      if(element.innerHTML.length == 0){
        Element.remove(element.parentNode);
      }
    }
  }
)
</script>

id を付けないとナニなので、無理矢理 span タグで囲んでいる模様。で、script.aculo.us の InPlaceEditor を使用している。ドキュメントはこれです。onComplete: オプションは、ドキュメントによると_Code run if update successful with server_との事。
対応するアクションは ajax コントローラの edit_item アクションで、item.id が渡される。又、処理が完了した時点で当該 id なオブジェクトの length が 0 (空文字列) になったらば、Element.remove を呼び出して削除しているらしい。

又、mozilla の「あなたのウェブページでウェブ標準を」というサイトによると、element.innerHTML というプロパティは W3C ドキュメントオブジェクトモデルのサポート範疇外、らしい。ただし、innerHTML は Gecko サポートとの事にてわしの端末ではぎりぎり動作しているんだな、と。
DOM 使って要素にアクセスせぇ、とあるが、document.getElementById とかを使って、該当 id なオブジェクトの length 取得ってできるんかな。別途調査および試験予定。

で、対応するアクションを以下に引用。(edit_item#ajax_controller.rb)

  def edit_item
    item = Item.find(params[:id])
    if params[:value].strip.empty?
      render_destroy item
    else
      item.note = params[:value]
      item.save
      render :text => item.note
    end

strip って何だ。String クラスかなぁ。(ruby 素人ッス。)
Ruby リファレンスマニュアル - String によると strip は空白文字を取り除く、とある。空文字列なら削除、と。
で、もう一つ気になるのは更新処理における render の意味。変更を反映、という風にも見えるんですが具体的に何なんだ、といいますか。Ajax.InPlaceEditor オブジェクトのコンストラクタに渡された id なオブジェクトの処理が完了した時点でソレを修正すんのかな? あ、scriptaculous wiki にこんな記述あり。_The server should respond with the updated value. (Ajax.InPlaceEditor in scriptaculous wiki より引用)_って微妙。

item の移動

ajax ならでは、の D&D による明細行の移動、とは言え model 的に position 列による sort がナニされてるので scriptaculous ではなく、ヘルパーメソドを使っている模様。この機能に関わる処理の定義は app/views/task/_list.rhtml にて定義。以下に一部を引用。

<%= form_remote_tag :url => {:controller => 'ajax', :action => 'add_item', :id =
> list.id },
    :failure => 'alert(request.responseText)',
    :complete => "Element.hide('add_item_#{ list.id }');
        Sortable.create('list_#{ list.id }',
        Sortable.options('list_#{ list.id }') );",
    :position => :bottom ,
    :update => { :success => "list_#{list.id}" } ,
    :html => {:style => 'display:none ;', :id => "add_item_#{list.id}" }
     %>
(中略)
<%= sortable_element "list_#{list.id}",
    :url => { :controller => 'ajax', :action => 'update_positions' },
    :with => "Sortable.serialize('list_#{list.id}', {name:'sortable_list'})" %>

下側に記述されている sortable_element がヘルパーメソドとなっており、rails/actionpack/lib/action_view/helpers/scriptaculous_helper.rb にて定義されている。以下にコメントを引用しておく。

Makes the element with the DOM ID specified by +element_id+ sortable by drag-and-drop and make an Ajax call whenever the sort order has changed. By default, the action called gets the serialized sortable element as parameters.

あと、Sortable.* については scriptaculous が出自な模様。ちょっとココは腰を据えて調べないとナニなので後回し。ポイントを以下に列挙。

  • Sortable.serialize の二番目の引数の引用符に囲まれた部分 (sortable_list) は sortable_element で指定されているアクション (update_positions#ajax_controller.rb) にて params の hash になっている。
  • params[:sortable_list] は配列になっている模様。(serialize??)
  • item 入力フォームにおいて、登録処理が正常終了した場合に、list.id 単位での Sortable なオブジェクトを create している。(む、complete 時のみで良いの??)
  • Sortable.create に渡している Sortable.options って何だ。
  • Sortable.serialize のドキュメント
  • Sortable.create のドキュメント

item の完了

app/views/task/_item.rhtml より一部を引用。

<li id='item_<%= item.id %>' 
    class='<%= item.completion ? 'completion' : 'not_completion' %>'>
<%= check_box_tag('check', '1', item.completion, 
                  :id => "completion_#{item.id}" ) %>
<%= observe_field "completion_#{item.id}",
  :url => {:controller => 'ajax', 
           :action => 'completion_item', 
           :id => item.id },
  :complete => %Q[$("item_#{item.id}").className == "not_completion"
                  ? $("item_#{item.id}").className = "completion"
                  : $("item_#{item.id}").className = "not_completion"]
  %>

li タグの class 指定は CSS で見栄えを調整していると見た。(根拠ナシ)
それは良いとして、observe_field というヘルパーメソドが面白い。ソースは rails/actionpack/lib/action_view/helpers/prototype_helper.rb なんですが、observ_field 定義部分のコメントの一部を以下に引用。

Observes the field with the DOM ID specified by +field_id+ and makes an Ajax call when its contents have changed.

コンテンツが変更されたらアクション実行ってコトになる模様。オプションに complete という hash が指定されているがおそらくはアクション実行が正常終了だった場合の処理になるはず。括弧の中身は class を変更している処理に読める。しかし %Q って何だ?? (って調べるの苦労した割に ruby の機能だし)

対応するアクションを以下に。(competion_item#ajax_controller.rb)

  def completion_item
    item = Item.find(params[:id])
    item.toggle! :completion
    render :nothing => true # 何も表示しない
  end

boolean なナニに対して便利なメソドが用意されているなぁ、と。

微妙に宿題は残っていますが、次は何やるかな。

  • acts_as_taggable をサンプルなブクマに実装
  • DynamicCalendarHelper 実装
  • todo 使って unit test
  • element じゃなくて DOM 使ってみる