Rails 3.0 + jQuery で RJS

本題に入る前に、そもそも RJS とは何なのかを説明すると、本来 jQuery の $.ajax() などを使ってごりごり書かないといけなかった JavaScript による非同期通信をフレームワーク側で吸収して、通常の html ビューのレンダリングと同じ感覚で記述できるように用意されたものです。
この「通常の html ビューのレンダリングと同じ感覚」というところは重要なポイントです。コントローラ内にも RJS のメソッドは直接記述できるのですが、本家 Railsガイドラインでは以下のように勧告しています。

Placing javascript updates in your controller may seem to streamline small updates, but it defeats the MVC orientation of Rails and will make it harder for other developers to follow the logic of your project. We recommend using a separate rjs template instead, no matter how small the update.

そういうわけなので、この記事では.rjs ファイルをビューとして使う前提で話を進めていきます。

準備

jQueryRails で使う場合 jquery-rails という gem を利用します。Gemfile を開いて、gem の使用を宣言して下さい。

gem 'jquery-rails'

bundle install で必要な gem がインストールされたら、jquery-rails のインストールを行います。

rails g jquery:install

この時、私の環境では OpenSSL::SSL::SSLError が発生してしまったので、以下のサイトのパッチを利用させてもらいました。

http://situated.wordpress.com/2008/06/10/opensslsslsslerror-certificate-verify-failed-open-uri/

まさにモンキーパッチ、という感じですが動作はしたので問題ないでしょう。なお、上のコードは config/application.rb の中に追加して、インストールが終わったらコメントアウトしておく事を推奨します。

準備はさらに続きます。デフォルトで読み込まれる JavaScript ファイルを設定するために config/application.rb を編集します。

config.action_view.javascript_expansions[:defaults] = %w(jquery.min rails)

読み込ませる順番はこの通りにしないと rails.js が jQuery のオブジェクトを発見できずにエラーが起こるので注意して下さい。

コードの作成

今回は Cost というモデルを使う事にします。このモデルは以下のフィールドを持ちます。

  • id
  • occured_on
  • amount
  • note
  • created_at
  • updated_at

まずはコントローラに index メソッドを定義します。

def index
  @costs = Cost.all
end

次に index.html.erb を作成。

<%= form_for(Cost.new, :remote => true) do |f| %>
  <p>
    <%= f.label :occured_on, '日付' %>
    <%= f.date_select :occured_on, :use_month_numbers => true %>
  </p>
  <p>
    <%= f.label :amount, '金額' %>
    <%= f.text_field :amount %>
  </p>
  <p>
    <%= f.label :note, 'メモ' %>
    <%= f.text_field :note %>
  </p>
  
  <p><%= f.submit "登録" %></p>
<% end %>

<%= render :partial => 'costs_table', :locals => {:costs => @costs} %>

Rails 3.0 からは remote_form_tag や link_to_remote は廃止され、上のコードのように :remote => true をつける事で非同期通信を行うようになっています。partial で指定した _costs_table.html.erb は以下のようになります。

<table id="costsTable">
  <thead>
    <tr>
      <th>日付</th>
      <th>金額</th>
      <th>メモ</th>
      <th>削除</th>
    </tr>
  </thead>
  <tbody>
    <% costs.each do |cost| %>
      <tr id="cost<%= cost.id %>">
        <td><%= cost.occured_on %></td>
        <td><%= cost.amount %></td>
        <td><%= cost.note %></td>
        <td><%= link_to '削除', cost, :remote => true, :confirm => 'よろしいですか?', :method => 'delete' %></td>
      </tr>
    <% end %>
  </tbody>
  <tfoot>
    <tr>
      <th colspan="3">合計金額</th>
      <td id="costsSum"><%= costs.inject(0){|sum, cost| sum + cost.amount} %></td>
    </tr>
  </tfoot>
</table>

削除のリンクに注目すると :remote => true と同時に :method => 'delete' と指定されているのが分かると思います。これは ActiveResource を利用したルーティングでは、メソッドに delete を指定したリクエストが削除の要求とみなされるためにつけています。今回はそちらの詳しい説明は省きますが、参考にして下さい。

話を戻して、上記 index.html.erb のフォームから呼び出される create アクションを costs_controller に定義します。

def create
  @cost = Cost.new(params[:cost])

  @cost.save
end

通常の html ベースの create とほとんど変わらないのが分かると思います。では、これに対応したビューを create.js.rjs というファイル名で通常のビューと同じ位置(この場合なら app/views/cost 以下)に作成しましょう。

if @cost.errors.any?
  page.alert(@cost.errors.full_messages.join("\n"))
else
  page.replace_html 'costsTable', :partial => 'costs_table', :locals => {:costs => Costs.all}
end

エラーが発生した場合はアラートで表示して、それ以外の場合は先ほど作った partial ファイルを使ってページを置き換えます。このように、直接 JavaScript を書く手間が省けるのと Railsレンダリングを最大限に活用できるのが RJS の魅力だと思います。

問題点

実は上のコード、記事執筆現在はそのままでは動きません。実際に試してみると Element.update が未定義だ、とお叱りを受けてしまいます。理由は明白で jQuery を使っているのに Prototype.js のコードを呼び出そうとしているから、つまり jquery-rails がそのあたりを吸収しきれていないからという事。結局ごりごり書くしかないのかと思って調べたら、すでに同じような問題に直面している方がいらっしゃいました。

http://chez-sugi.net/journal/20100907.html

なるほどエクセレント!早速この方法を採用させてもらい public/javascripts/application.js に以下のように記述しました。

Element.update = function (element, html) {
  $('#' + element).html(html);
}

ページを再読み込みしてフォームから送信すると、きちんと RJS が動作する事を確認できるはずです。お疲れさまでした!