ADZ 學習筆記

Ruby/Rails, Startup, Life

Rails Sharing #2 - 當 controller create 來自不同頁面時的處理

| Comments

現在要討論的問題是,如果我想在一個 resource 裡面的 #index 有一個簡化的新增表單、#new 裡面是一個完整的新增表單。

在 #index 裡面的做法其實跟 #new 是一樣的,真正的問題是,當送過去 #create 時,如果 validate 不過的處理。

app/controllers/posts_controller.rb
# POST /posts

def create
  @post = Post.new(post_params)
  if @post.save
    redirect_to some_where_path, notice: 'successful'
  else
    render :new # 重點是這個地方

  end
end

在 #7 行render :new 的地方,如果你的流程設計是從 #index post 過來 validate 不過時,就重新 render :new 的話,以上程式碼完全沒問題 (某些網站是這樣做的,比如說:首頁有登入畫面,如果登入失敗則離開首頁,顯示完整登入的 view)。

但如果你希望設計的流程是,在 #index 填寫送出新增 validate 不過維持在 #index,#new 填寫送出新增不過則 re-render :new 的話,那就必須在 #create 裡面做特別處理了 (至少要讓 create 知道該怎麼 re-render view)

不過這又會有另一個問題,render 只是幫你去 render 某個 view 並不會幫你跑屬於那個 view 的 action,所以假設我有以下程式碼,就會有更多邏輯要處理。

app/controllers/account/posts_controller.rb
# GET /account/posts

def index
  @post = current_user.posts.newest.page(params[:page])
end

# GET /account/posts/new

def new
  # .. (略)

end
app/controllers/account/posts_controller.rb
# GET /account/posts

def create
  @post = Post.new(post_params)
  if @post.save
    redirect_to some_where_path, notice: 'successful'
  elsif params[:from_action] == 'index'
    render :index
  elsif params[:from_action] == 'new'
    render :new
  end
end

然後再 #index 的 form 裡面這樣實作:

app/views/account/posts/index.html.erb
<%= form_for([:account, @post]) |f| %>
  <input type="hidden" name="form_action" value="index">
<% end %>

我們先不討論 from_action 的部分,上面的程式碼當你 validate 不過時判斷從哪裡 post 過來的問題是:重新 render 了 :index view 但這時並沒有跑 #index 的 action,等於你跑了那個 view 卻沒有 @posts,當然你可以加上。

def create
  # ... (略)

  @posts = current_user.posts.newest.page(params[:page])
  render :index
  # ... (略)

end

這種情況假設你的 PostController#index 要做的事情越多,你的程式碼越髒,另一種會讓你更難維護的狀況是,假設你的 form 擺在各種不同的地方,假設:首頁放一個、posts#index 放一個、聯絡我們也放一個。以上這種方法就會非常不好維護。

所以如果是這種狀況我的做法會以 "有幾種 form 來切割" 去取代以上的 "以幾種來源來切割",假設整個網站的 posts#create 的 form 總共有兩種,一種精簡的、一種完整的,再搭配 SRJ (Server side response javascript) 的做法,利用 AJAX 只更新正在操作的那張 form html 就可以了,我的 form 會有以下兩張。

# app/views/account/posts/_lean_form.html.erb (精簡的 form)
<%= form_for([:account, @post], remote: true, format: :js, html: { id: 'post_lean_form' }) do |f| %>
  <input type="hidden" name="form_type" value="lean_form">
  <!-- 略 --!>
<% end %>

# app/views/account/posts/_form.html.erb (完整的 form)
<%= form_for([:account, @post], remote: true, format: :js, html: { id: 'post_form' }) do |f| %>
  <!-- 略 --!>
<% end %>

有需要精簡表單的,我可以在任意地方 render 'account/posts/lean_form'

# app/views/account/posts/new.html.erb
<h1>Hello New page</h1>
<%= render 'new' %>

# app/views/home/index.html.erb
<h1>Home Page</h1>
<%= render 'account/posts/lean_form' %>

接下來才是重頭戲,我要如何使用 SRJ 去 handle post request 而不去刷新頁面。

app/controllers/account/posts_controller.rb
# POST /account/posts.js

def create
  respond_to do |format|
    # 僅允許 .js 的 request

    format.js {
      @post = current_user.posts.new(post_params)
      @status = @post.save
    }
  end
end

當你限定 #create 僅允許 js 時,其他 format 的 request 都會被以 UnknowFormat 的 exception 擋下來,跟之前做法的差異是,我們不在controller 內處理 redirect / re-render 邏輯,而是把 valid 的結果存到 @status,留到 javascript 去控制瀏覽器,接下來我們需要實作 create.js.erb

app/account/posts/create.js.erb
// SRJ 這裏習慣用 function 包起來在執行避免污染 javascript 全域變數
(function(){
  <% if @status %>
    window.location.href = '<%= post_path(@post) %>';
  <% else %>
    <% if params[:form_type] == 'lean_form' %>
      $form = $('<%=j render 'lean_form' %>');
      $('#post_lean_form').html($form.html());
    <% else %>
      $form = $('<%=j render 'form' %>');
      $('#lean_form').html($form.html());
    <% end %>
  <% end %>
});

以上的 #create 如果 save 成功,則瀏覽器會執行導頁到 posts/:id 的頁面,而 valid 不過的狀況,會依照是哪個類型的 form 去選擇 render 在 replace 掉畫面上的 form,這樣一來就完全不必管,是從哪裡來,還要特別處理那些撈資料的問題了。

完成 :)

Comments

comments powered by Disqus