2008年8月1日金曜日

Ruby on Rails + jQuery + Ajax で動的にページの一部を更新する

例えばTwitterでブラウザからつぶやきを投稿すると、ページを再読み込みしないで投稿したつぶやきだけが動的に追加されます。
あんな感じの処理をRails + jQueryで書いてみたので、そのメモをば。
Twitterのようなブログ系のアプリを想定しています。

大まかな流れ

1. フォームに記入された入力値をAjaxでサーバーに送信。
2. 入力値を検証。問題がなければ成功フラグを、問題があればエラーフラグをJSONでクライアントに返す。
3. 問題がなければJavascriptで新しい項目を追加。

Ajaxでページの一部を動的に更新する場合は普通こんな流れになると思います。1と3がクライアント(ブラウザー)サイド、2がサーバーサイドの処理です。

1. フォームに記入された入力値をAjaxでサーバーに送信

index.html.erb (railsのview)

<% form_for @post, :html => {:onsubmit => "return save(this);"} do |f| %>
<%= f.text_area :body, 'cols' => 40, 'rows' => 5 %>
<%= f.submit "投稿" %>
<div id="post_status" style="display:none;"></div>
<% end %>

<%div id="posts"%>
<%= render :partial => "post", :collection => @posts %>
<%/div%>

new.html.erbのような投稿専用のviewを用意しないで、ブログの一覧を表示するviewにフォームを設置します。なぜって?そうしなきゃAjaxで投稿するメリットがありませんから。

まずformのonsubmitを指定します。
もちろんjQueryの$(document).ready()で設定することも可能です。
また、送信のステータスを表示するDIV要素を仕込んでいます。

render :partialで_post.html.erb(記事部分)をレンダリングします。:collectionを指定することで@postsの中身をループ処理することができます。この部分はこの一連の処理の隠れたキモになるので注意。

_post.html.erb
<div class="post">
<%=h post.body %>
</div>

partialのviewには記事ひとつ分の記述をします。

Javascript
function save(f){
$.ajax({
url: $(f).attr('action'),
type: 'POST',
dataType: 'json',
timeout: 1000,
data : $(f).serialize(),
beforeSend : function(){ $('#post_status').html('送信中...').fadeIn(200); },
error: function(){alert('Error Occured');},
success: function(obj){
//通信が成功した場合の処理
}
});
return false;
}

saveはform要素を引数にとる関数です。
jQueryのserialize()を使ってフォームのデータをシリアライズ(a=1&b=2の形式)しています。
ちなみにserializeArray()という似たようなメソッドがありますが、こちらはフォームの入力値をハッシュ({a:1, b:2}の形式)にしてくれます。

2. サーバーサイド(Rails)の処理
モデルのバリデーション等はここでは省略します。

posts_controller.rb
def create
return redirect_to '/404.html' unless request.xhr?
post = Post.new(params[:post])
if post.save
html = render_to_string :partial => "post", :collection => [post]
render :json => {:success => 1, :html => html}
else
render :json => {:error => post.errors}
end
end

とりあえずAjax以外のアクセスは弾くという前提で。
return redirect_to '/404.html' unless request.xhr?
と書くと、Ajax以外のアクセスを404.htmlにリダイレクトしてくれます。

render_to_stringがこの一連の処理のキモかもしれません。というかこのメソッドを使わないとJavascirptで一生懸命DOMを書いたりしなければならなくなるでしょう。
さきほど作ったindex.html.erb内のrender :partialで指定したのと同じpartialテンプレート(_post.html.erb)をここで指定していることに注目してください。
記事の中身をpartialでレンダリングすることで、viewを使い回すことができます。
:collectionにpostを配列に入れて渡していることも注意。

3. 新たに投稿された記事部分を更新する

Javascript
function save(f){
$.ajax({
[中略]
success: function(obj){
//通信が成功した場合の処理
if(obj.error){
var html = '';
$(obj.error).each(function(){ html += this[1] + '<br />'; });
$('#post_status').text(html).css(color, 'red');
}else if(obj.success){
$('#post_status').text('正常に登録しました').css(color, 'green');
$('#posts').append(obj.html);
}
}
});
return false;
}

通信や入力値に問題がなければ、htmlを描画します。obj.htmlにはrender_to_stringでレンダリングしたhtmlが丸ごと格納されているので、後はそれをしかるべき場所に挿入するだけです。

3 件のコメント:

shinriyo さんのコメント...

参考にさせていただいてます。
しかし、:onsubmitがうまくいきません。Rails3から変わったのでしょうか?

Youthhr さんのコメント...

onsubmitをインラインで記述せずに、$('#form_id').submit() と書いてみたらどうでしょうか

shinriyo さんのコメント...

すみません。まだきちんとした動作はしないものの、いつの間にかonsubmitに関してはうまくいくようになってました。
JavaScriptファイルはhoge.js.erbというファイルにするのがRailsの世界では一般的なのでしょうか?
_post.html.erbの「post.body」の「post」は<%div id="posts"%>のところの:collection => @posts によって中身を展開したものを渡しているという解釈でよろしいでしょうか?
素人な質問ですみません。