Hai mẹo nhỏ với fields_for và CarrierWave

Trong lúc làm dự án, mình có gặp một số vấn đề với fields_for và việc upload lại file khi submit form có chứa lỗi. Sau khi google thần chưởng thì mình đã tìm được giải pháp và quyết định đăng lên Viblo để những ai chưa biết có thể tham khảo 😄

1. Sử dụng fields_for với cùng một object ở nhiều nơi

Giả sử bạn có một model Book có 2 trường authornum_of_page và model BookTranslation belongs_to Book và có 3 trường language, name, description dùng để hiện thông tin về namedescription của Book ở một language nào đó. Trong một form tạo mới book, bạn cần tạo Book với một language mặc định ban đầu và thứ tự hiển thị các trường trên form như sau:

  • name
  • author
  • num_of_page
  • description

Như vậy ta sẽ phải dùng fields_for ở 2 chỗ cho cùng 1 object book_translation:

<%= form_for @book do |f| %>
  <%= f.fields_for :translations, @book_translation do |ff| %>
    <%= ff.label :name, "Name" %>
    <%= ff.text_field :name %>
  <% end %>

  <%= f.label :author, "Author" %>
  <%= f.text_field :author %>

  <%= f.label :num_of_page, "Num of page" %>
  <%= f.number_field :num_of_page %>

  <%= f.fields_for :translations, @book_translation do |ff| %>
    <%= ff.label :description, "Description" %>
    <%= ff.text_field :description %>
  <% end %>
<% end %>

Điều này sẽ khiến params có dạng như sau:

{"num_of_pages"=>"123", "author"=>"AuthorA", "translations_attributes"=>{"0"=>{"name"=>"Book 1"}, "1"=>{"description"=>"Description 1"}}}

Như vậy khi lưu book thì sẽ tạo ra 2 book_translation, một book_translation sẽ có name, không có description và book_translation kia thì ngược lại, có description mà không có name. Vậy làm thế nào để kết hợp các giá trị trong 2 cặp key-value "0"=>{"name"=>"Book 1"}"1"=>{"description"=>"Description 1"}? Cách giải quyết là thêm tùy chọn child_index với giá trị giống nhau ở những lời gọi fields_for:

<%= form_for @book do |f| %>
  <%= f.fields_for :translations, @book_translation, child_index: 0 do |ff| %>
    <%= ff.label :name, "Name" %>
    <%= ff.text_field :name %>
  <% end %>

  ...

  <%= f.fields_for :translations, @book_translation, child_index: 0 do |ff| %>
    <%= ff.label :description, "Description" %>
    <%= ff.text_field :description %>
  <% end %>
<% end %>

Params sinh ra sẽ là:

{"num_of_pages"=>"123", "author"=>"AuthorA", "translations_attributes"=>{"0"=>{"name"=>"Book 1", "description"=>"Description 1"}}}

Như vậy sẽ chỉ có một book_translation được tạo ra.

2. Tự động lấy ảnh đã được tải lên server để submit lại form khi form bị lỗi trước đó (sử dụng gem CarrierWave)

Giả sử Book của bạn có thêm ảnh (sử dụng CarriewWave upload và lưu trữ), và form tạo book cần upload ảnh lên, nhưng form bạn submit lên có chứa lỗi (chẳng hạn yêu cầu nhập author mà author submit lên lại rỗng) thì khi form được render lại bạn sẽ không thấy ảnh đã upload lên lúc trước và phải upload lại ảnh. Điều này khá là phiền toái, và gem CarrierWave có một cách giải quyết trường hợp này như sau:

Model Book:

...
mount_uploader :image, ImageUploader
...

Form tạo:

...
<%= f.file_field :image, accept: "image/png,image/gif,image/jpeg" %>
<%# Khai báo thêm dòng này %>
<%= f.hidden_field :image_cache %>
...

Với hidden_field trên, khi submit form thì ảnh đã được tải lên từ lần submit trước sẽ được sử dụng, bạn không cần phải tải lên lại ảnh bạn muốn nữa. Chú ý là khi sử dụng strong parameter thì bạn cần khai báo thêm :image_cache trong danh sách parameter để có thể lưu được:

params.require(:book).permit :author, :num_of_page, :image, :image_cache, ...

3. Tổng kết

Qua bài viết trên, mình đã trình bày 2 mẹo nhỏ khi sử dụng fields_for và gem CarrierWave, hy vọng sẽ giúp ích được cho mọi người trong công việc. Cảm ơn mọi người đã theo dõi 😄