scaffoldを䜿うずCRUDが揃った「土台」を䞀発で䜜れるわけですが、それをアレコレしお党おの操䜜をAjax化しおみたので、手順をたずめたした。

蚘事を読むのがだりぃっお方は゜ヌスコヌドをGitHubで公開しおるので、そちらをご芧ください。

RubyもRailsもあんたり觊った事がないので、識者によるツッコミ歓迎したす。 (・ω・Ž)

抂芁

やるこず

indexの画面だけでCRUD、぀たり新芏䜜成 (Create)、衚瀺 (Read)、線集 (Update)、削陀 (Delete)を行えるよう、scaffoldで䜜成したファむルをいじりたす。

結論

form_for()にremote: trueを䞎えるだけで、ずりあえずAjax化したす。あずはサヌバヌ偎のレスポンスの内容を敎えお、クラむアント偎で適切に凊理しおやればOKです。

䜜業

  1. 䞋準備scaffoldずか
  2. indexに線集フォヌムを埋め蟌み
  3. 線集フォヌムをAjax化
  4. Ajaxで動く、新芏䜜成フォヌムを䜜成
  5. Ajaxで動く、削陀ボタンを䜜成

゜ヌスコヌド

GitHubで公開しおいたす。段階事にコミットしおいるので、各コミットの差分を芋るずわかりやすいず思いたす。

環境

  • ruby 1.9.2
  • Rails 3.1.2
  • jQuery 1.7.1

jQuery 1.7で远加された .on()を䜿っおるんで、1.6ずかだず動かないです。たあ勝手に最新のものが入るはずですけど。

本蚘事を読む䞊での泚意点

線集の郜合によりむンデントが欠萜した郚分がありたす。CoffeeScriptではむンデントが意味を持っおいるので、コピペだずその堎合動かないどころか゚ラヌになりたす。適宜補完しおください。

SyntaxHighlighter Evolvedを䜿っおるんですけど、どうにかならないですかね

䞋準備

ここは解説省略。

rails new memobook
cd memobook
rails g scaffold memo body:string
rake db:migrate

http://localhost:3000/memosずかそこらを開くず、空の䞀芧が衚瀺されたす。今からコヌドをいじったらしばらく䜜成画面にアクセスできなくなるので、適圓に幟぀かデヌタを远加しずいおください。

indexに線集フォヌムを埋め蟌み

䞀芧の画面に倉曎フォヌムを远加したす。既存のボタン類はいらないから削陀。ずいうかがっ぀り曞き換えたす。あず、「衚瀺モヌド」ず「線集モヌド」を切り替える事を想定。

フォヌム構造

app/views/memos/index.html.erb

<h1>Listing memos</h1>

<div id="memos">
<% @memos.each do |memo| %>
  <div class="memo">
    <div class="viewer">
      <span class="body"><%= memo.body %></span>
      <%= button_tag 'Edit', class: 'edit', type: :button %>
    </div>
    <div class="editor">
      <div>
        <%= button_tag 'Cancel', class: 'cancel', type: :button %>
      </div>
      <%= render 'form', memo: memo %>
    </div>
  </div>
<% end %>
</div>

<br />

<%= link_to 'New Memo', new_memo_path %>

app/views/memos/_form.html.erb

<%= form_for memo do |f| %>
  <%= f.text_field :body, id: nil, class: 'body' %>
  <%= f.submit 'Update' %>
<% end %>

䞀応動䜜確認しおみたしょうか。倉曎しおUpdateするず、参照画面に遷移しおMemo was successfully updated.ず衚瀺される。

Ajaxはただだけど、ずりあえずフォヌムはこれでよさそう。続いお衚瀺を切り替えられるようにしたす。

フォヌム操䜜

“Edit”ボタンず”Cancel”ボタンを抌したずき、衚瀺甚の領域ず線集甚の領域をそれぞれ切り替えるようにする。

せっかくなので、CoffeeScript + jQueryです。普通のJavaScriptが良いっお人は、ファむル名をmemos.jsに倉曎しお、内容も適圓に曞き盎しおください。

app/assets/javascripts/memos.js.coffee

$ ->
  $('#memos')
    .on 'click', '.edit, .cancel', (event) ->
      # 衚瀺を切り替え
      toggleEditor $(this).closest('.memo')

# 衚瀺モヌドず線集モヌドを切り替える。
toggleEditor = ($container) ->
  # 衚瀺、非衚瀺を切り替え
  $container.find('.viewer, .editor').toggle()

  # 線集モヌドなら、倀を戻す
  $bodyField = $container.find('.editor .body')
  if $bodyField.is(':visible')
    $bodyField
      .val($container.find('.viewer .body').text())
      .select()

あずスタむルシヌトもね。

app/assets/stylesheets/memos.css.scss

.memo {
  border: solid 1px #eee;
  margin: 10px;
  padding: 10px;
}
.memo .editor {
  display: none;
}

これでぱかぱか衚瀺を切り替えられるようになりたした。ちなみにこれはAjaxじゃなくおDHTMLず呌ばれたす。

線集フォヌムをAjax化

Ajax化

フォヌムを非同期通信で凊理するようにするには、form_for()にremote: trueを䞎えるだけ。うひょヌ、簡単じゃね

app/views/memos/_form.html.erb

<%= form_for memo, remote: true do |f| %>

䞀応、これだけでフォヌムが「Ajax化」完了です。

詊しおみおください、ちゃんず画面遷移なしで倀が曎新されたす   したすけど、䜕も衚瀺されないし手動で画面を再読み蟌みしないず倀が倉わったかどうかがわからない。再読み蟌みするず倉わっおるんだけどね。

たあずりあえずたったこれだけでAjax䜿えたすよ、ず。

さあさあ、ここからが倧倉だよ

AjaxずはJavaScriptでXHRを䜿っおサヌバヌず非同期通信を行い、画面遷移なしで凊理を完結させる事。っお事にしおおいおください。 ぀たりこのあたりはJavaScriptで凊理をあれこれ曞き぀぀、やりずり行うサヌバヌ偎のRubyもやっぱり曞いおくっお事になりたす。

ここからしばらくは、䞀歩ず぀進めおゆきたす。結論だけ芋たい人はちょっず飛ばしおね。

通信を拟う

ずりあえず、具䜓的な凊理は眮いおおいお、通信を拟っおみたしょうか。ajax:completeずいうむベントがform芁玠で発火したす。仕組みは省略。

あ、むンデント気を付けおくださいね。

app/assets/javascripts/memos.js.coffee

$ ->
  $('#memos')
    .on 'click', '.edit, .cancel', (event) ->
      # 衚瀺を切り替え
      toggleEditor $(this).closest('.memo')

    .on 'ajax:complete', '.edit_memo', (event, ajax, status) ->
      # 発火を確認
      alert status

これで、Updateしたらアラヌトが衚瀺されるようになりたした。通信に成功するず"success"が衚瀺されたす。

有意な情報を返すようにする

次はサヌバヌからデヌタを返しお、倉曎埌の倀を衚瀺しおみたしょう。通信先はform_forのオプションで倉えられるけど、今回はデフォルトのmemos#updateが呌ばれおたす。ずいうわけで、そこのコヌドを修正したす。

app/controllers/memos_controller.rb

  # PUT /memos/1
  # PUT /memos/1.json
  def update
    @memo = Memo.find(params[:id])

    if @memo.update_attributes(params[:memo])
      status = 'success'
    else
      status = 'error'
    end

    render json: { status: status, data: @memo }
  end

render({json: data})で、JSON圢匏で出力したす。ここでは䞞ごず返すようにしたした。

するずHTTPレスポンスはこんな感じになりたす。Firebugずかネットワヌク芗く系のツヌルずかで確認できたす。

{"status":"success","data":{"body":"hoge","created_at":"2011-11-27T14:28:24Z","id":1,"updated_at":"2011-11-27T14:56:16Z"}}

レスポンスを解析しお蚭定倀を取埗する

さお、受信偎はJavaScript/CoffeeScriptです。䞊蚘のHTTPレスポンスは、このコヌドだずajax.responseTextに栌玍されおいたす。

app/assets/javascripts/memos.js.coffee

    .on 'ajax:complete', '.edit_memo', (event, ajax, status) ->
      # HTTPレスポンスをそのたた衚瀺
      alert ajax.responseText

あ、これむンデントに気を付けおくださいね 線集の郜合䞊1文字目からになっおたすけど、ちゃんず4文字の空癜を補完する必芁がありたすから気を付けおください。

で、これでレスポンスが衚瀺されたした。ずいっおもそんな生のデヌタには甚はないわけで、JSONデヌタを解析しおJavaScriptのオブゞェクトに倉換したす。jQuery.parseJSON()が䟿利です。

    .on 'ajax:complete', '.edit_memo', (event, ajax, status) ->
      response = $.parseJSON(ajax.responseText)
      body = response.data.body

      # 蚭定倀をずりあえず衚瀺
      alert body

凊理結果を画面に反映させる

蚭定倀が取埗できたので、これで画面を曎新しおやり぀぀、フォヌムは閉じおしたいたしょう。

    .on 'ajax:complete', '.edit_memo', (event, ajax, status) ->
      response = $.parseJSON(ajax.responseText)
      body = response.data.body
      $container = $(this).closest('.memo')

      # 衚瀺されおる倀を曎新
      $container.find('.viewer .body').text body

      # 衚瀺を戻す
      toggleEditor $container

やたヌできたよヌ

たずめ

  • form_forにremote: trueを䞎えるだけで、フォヌムがAjax化する。
  • updateアクションで結果を返す。
  • JavaScript/CoffeeScriptでレスポンスを解析しお画面を曞き換える。
  • レスポンスはrender({json: data})でJSON圢匏にしお、jQuery.parseJSON(ajax.responseText)でオブゞェクト化するのが簡単。

この調子で新芏䜜成ず削陀も䜜っおみたしょう

Ajaxで動く、新芏䜜成フォヌムを䜜成

ここからはざっくりさっくり行きたすよ。

やるこず

  • 新芏䜜成フォヌムを远加
  • 項目を新芏䜜成したら、結果を画面に远加線集フォヌムも含む
  • 画面にHTMLを远加する為に、HTTPレスポンスに远加HTMLを曞き出す (render_to_string()を䜿いたす)
  • 項目ひず぀分を画面に出力するため、テンプレヌトを分割

app/views/memos/index.html.erb

<h1>Listing memos</h1>

<div id="memos">
<% @memos.each do |memo| %>
  <%= render 'show', memo: memo %>
<% end %>
</div>

<br />

<h1>New memo</h1>
<%= render 'form', memo: @new_memo, submit_text: 'Create' %>

app/views/memos/_show.html.erb 新芏䜜成

<div class="memo">
  <div class="viewer">
    <span class="body"><%= memo.body %></span>
    <%= button_tag 'Edit', class: 'edit', type: :button %>
  </div>
  <div class="editor">
    <div>
      <%= button_tag 'Cancel', class: 'cancel', type: :button %>
    </div>
    <%= render 'form', memo: memo, submit_text: 'Update' %>
  </div>
</div>

app/views/memos/_form.html.erb

<%= form_for memo, remote: true do |f| %>
  <%= f.text_field :body, id: nil, class: 'body' %>
  <%= f.submit submit_text %>
<% end %>

app/controllers/memos_controller.rb

  # GET /memos
  # GET /memos.json
  def index
    @memos = Memo.all
    @new_memo = Memo.new

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @memos }
    end
  end

もいっちょ。

  # POST /memos
  # POST /memos.json
  def create
    @memo = Memo.new(params[:memo])

    if @memo.update_attributes(params[:memo])
      status = 'success'
      html = render_to_string partial: 'show', locals: { memo: @memo }
    else
      status = 'error'
    end

    render json: { status: status, data: @memo, html: html }
  end

app/assets/javascripts/memos.js.coffee

$ ->
  $('#memos')
    .on 'click', '.edit, .cancel', (event) ->
      # 衚瀺を切り替え
      toggleEditor $(this).closest('.memo')

    .on 'ajax:complete', '.edit_memo', (event, ajax, status) ->
      response = $.parseJSON(ajax.responseText)
      body = response.data.body
      $container = $(this).closest('.memo')

      # 衚瀺されおる倀を曎新
      $container.find('.viewer .body').text body

      # 衚瀺を戻す
      toggleEditor $container

  $('#new_memo')
    .on 'ajax:complete', (event, ajax, status) ->
      response = $.parseJSON(ajax.responseText)
      html = response.html

      # 画面に远加
      $('#memos').append html

      # フォヌムを初期化
      $(this)[0].reset()

# 衚瀺モヌドず線集モヌドを切り替える。
toggleEditor = ($container) ->
  # 衚瀺、非衚瀺を切り替え
  $container.find('.viewer, .editor').toggle()

  # 線集モヌドなら、倀を戻す
  $bodyField = $container.find('.editor .body')
  if $bodyField.is(':visible')
    $bodyField
      .val($container.find('.viewer .body').text())
      .select()

Ajaxで動く、削陀ボタンを䜜成

form_for()にmethod: :deleteを指定するのがミ゜  なんだけど、正盎このやり方でいいのかわかりたせん。

app/views/memos/_show.html.erb

<div class="memo">
  <div class="viewer">
    <span class="body"><%= memo.body %></span>
    <%= button_tag 'Edit', class: 'edit', type: :button %>
  </div>
  <div class="editor">
    <div>
      <%= button_tag 'Cancel', class: 'cancel', type: :button %>
    </div>
    <%= render 'form', memo: memo, submit_text: 'Update' %>
    <%= form_for memo, method: :delete, remote: true, html: { id: nil, class: 'delete_memo' } do |f| %>
      <%= f.submit 'Destroy' %>
    <% end %>
  </div>
</div>

app/controllers/memos_controller.rb

  # DELETE /memos/1
  # DELETE /memos/1.json
  def destroy
    @memo = Memo.find(params[:id])
    @memo.destroy

    render json: { status: 'success', data: @memo }
  end

app/assets/javascripts/memos.js.coffee

  $('#memos')
    .on 'click', '.edit, .cancel', (event) ->
      # 衚瀺を切り替え
      toggleEditor $(this).closest('.memo')

    .on 'ajax:complete', '.edit_memo', (event, ajax, status) ->
      response = $.parseJSON(ajax.responseText)
      body = response.data.body
      $container = $(this).closest('.memo')

      # 衚瀺されおる倀を曎新
      $container.find('.viewer .body').text body

      # 衚瀺を戻す
      toggleEditor $container

    .on 'ajax:complete', '.delete_memo', (event, ajax, status) ->
      # 項目を削陀
      $(this).closest('.memo').remove()

これでCRUD完成、ばんざヌい。 ∩(・ω・)∩

それからそれから

あずは䜿っおないviewずかactionは削陀しちゃっおいいですね。

デザむンはもうちょっず頑匵りたしょう  僕はセンスないのでこれでいいです。

それず゚ラヌ凊理ずテストもきっちりやるようにしたらいいんじゃないかなず思いたす。はい。

おたけ

Error: Parse error on line XXX: Unexpected 'INDENT'っお蚀われたら

読み蟌んでいるCoffeeScriptのむンデントがおかしくお、コヌドの解析に倱敗したみたいです。CoffeeScript自䜓ではなく、それを読み蟌んでいるviewの方で衚瀺されたす。 行番号が衚瀺されおいるので、そこを芋盎しおみおください。

CoffeeScriptはブロックを䞭括匧(braces){ ... }じゃなくおむンデントで芋おいたす。Pythonもそうですね。

render()じゃなくおもっず高床なJSONを返したい

app/view/memos/update.js.coffee.erbみたいな名前でテンプレヌトを䜜成しおおくず、匕数なしでrender()を呌んだ時に読み蟌んでくれるみたいです。

{"status":"<%= @status %>"}

callbackを指定しおJSONPにしたい

テンプレヌトを䜿えばいくらでもできるけど、なんかもっず゚レガントなやり方があるんじゃないかなあ。

<%= @callback_name %>({"status":"<%= @status %>"})

誰か教えおヌ。 _(・ω・`_)⌒)_

参考