Thao tác với Forms trong Rails 4
Bài đăng này đã không được cập nhật trong 8 năm
Khi phát triển ứng dụng web nói chung và với Rails nói riêng, có thể có khi chúng ta gặp trường hợp khi chúng ta dùng nested form
với @post
has_many comments
mà chúng ta cần submit lưu các comments
tại 1 tab
sau đó submit
tab
form @post chính thì cần lưu các comments vừa save thuộc vào @post
. Chú ý là chỉ có tab
chính của form @post
mới submit theo dạng html
hoặc ajax
, tất cả các tabs
còn lại đều submit dùng JS
. (bởi vì chỉ khi submit form chính để tạo post
, /posts/new
).
- Submit từng tab, sau đó submit tab form chính để lưu
post
.
Cuối cùng sau khi submit thì chúng ta cần show
ra post
và các comments
của nó như khi đã điền vào form.
Mô hình CSDL
Để tiện theo dõi mình sẽ tạo mô hình như đã nêu phía trên là Post
và Comment
.
Khởi tạo mô hình
Tạo model Post
.
$ rails g scaffold post title:string content:text
Tạo model Comment
$ rails g scaffold comment content:text
Migrate database
$ rake db:migrate
Thiết lập mối quan hệ
Ở đây có duy nhất một mối quan hệ là Post
có nhiều Comments
. Vì vậy mình sử dụng nested-form
cho việc tạo form.
Mình dùng gem cocoon
cho việc build form nested này. Có thể dùng gem nested-form
nhưng mình dùng gem cocoon
vì nó mới và nhẹ hơn.
Bài viết này mình không đi vào việc cài đặt và sử dụng gem kia như thế nào. Mặc định là đã dùng được rồi nhé.
Chỉnh sửa chút về Post.rb
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments, allow_destroy: true,
reject_if: :all_blank
end
Với model Comment
class Comment < ActiveRecord::Base
belongs_to :post
end
Như vậy là mình đã cài đặt xong quan hệ. Phần tiếp theo mình sẽ trình bày về xây dựng trang posts/new
với 2 tabs gồm tab chính chứa form @post, tab bên là nested-form
cho @post - comments.
Tiếp theo với chủ đề này. Ở phần đầu mình đã nêu ra vấn đề và thiết lập mô hình.
Bài viết này mình sẽ trình bày về tạo form. Từng bước một để mọi người tiện theo dõi.
Tạo trang new
post với 2 tabs
Ở đây mình dùng bootstrap
.
Đầu tiên cần thêm vào route.rb
resources :posts
resources :comments
Mình sẽ viết theo cú pháp slim-rails
cho code ruby.
Tiếp theo mở controllers/posts_controller.rb
class PostsController < ApplicationController
def new
@post = Post.new
end
end
Mở views/posts/new.slim
.tabs-container
ul.nav.nav-tabs
li.active
a aria-expanded="true" data-toggle="tab" href="#basic-information"
= t(".basic_information")
li
a aria-expanded="false" data-toggle="tab" href="#comments"
= t(".comments")
.tab-content
#basic-information.tab-pane.active
.panel-body
= render "form", post: @post
#comments.tab-pane
.panel-body
= render partial: "comments_form",
locals: {post: @post}
Ở phía trên mình tạo ra 2 tabs
, một tab đầu tiên là chứa form
chính cho điền các thông tin về post
, tab thứ hai là chứa
form
để điền thông tin về các comments
cho post
.
Tạo views/posts/_form.slim
= simple_form_for post, html: {class: "form-horizontal"} do |f|
= f.input :title, label: t(".title")
= f.input :content, label: t(".content")
= f.submit t(".save"), class: "btn btn-w-m btn-primary"
Tạo views/posts/_comments_form.slim
#comments-form
= simple_form_for post do |f|
= f.hidden_field :id
#comments
= f.fields_for :comments do |comment|
= render partial: "shared/comment_fields",
locals: {f: comment}
.links.text-center.margin-top-10
= link_to_add_association f, :comments, partial: "shared/comment_fields",
class: "btn btn-default" do
span.fa.fa-plus
=< t(".add_sample")
.text-center.margin-top-20
= link_to "#!", class: "btn btn-w-m btn-primary",
data: {disable_with: t(".submitting")} do
= t(".save")
Như vậy mình đã xây dựng 2 forms cho 2 tabs tại trang posts/new
. Ở đây mỗi tab là một form
với chung một object là @post
. và đều có nút submit riêng.
Ở phần tiếp theo mình sẽ trình bày tiếp về phần submit riêng tab comments_form
trước để lưu trữ các comments nhưng chưa thuộc vào post
nào cả, sau khi bấm submit tại form
post chính thì mới lưu post và liên kết các comments đã tạo ở tab bên kia vào post này.
Và không load lại trang khi submit tạo comments. Chỉ redirect_to post_path @post
khi submit lưu post
tại form chính.
Tiếp tục với phần bài viết về submit multiple tabs form, phần này mình trình bày về chức năng submit lần lượt từng tab, sử dụng ajax để lưu cho các tab phụ và submit sử dụng html cho tab form chính.
Ở phần trước mình đã trình bày về tạo form cho 2 tabs là form post và comment form.
Vì chúng ta sẽ sử dụng ajax
để submit form lưu comments
, khi này comments được lưu nhưng chưa thuộc về post
nào cả. Sau khi tạo xong comments
người dùng submit form post thì bắt đầu lưu post và liên kết các comments đã tạo ở form kia vào trong post mới tạo này.
Nhìn lại relation ta có
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments, allow_destroy: true,
reject_if: :all_blank
end
class Comment < ActiveRecord::Base
belongs_to :post
end
Vậy để liên kết các comments đã có vào trong post khi tạo chúng ta sẽ thêm chút sau vào controller.
# posts_controller.rb
class PostsController < ApplicationController
def new
@post = Post.new post_params
end
def create
@post = Post.create post_params
redirect_to post_path @post
end
private
def post_params
params.require(:post).permit :title, :content, comment_ids: []
end
end
Sử dụng params chứa comment_ids: []
là một array chứa các ID
của comments đã tạo ở tab bên kia nhằm mục đích liên kết nó vào post
khi tạo ở tab này.
Tiếp theo chúng ta đến với việc tạo các comments ở tab comment_form.
Vì đây chúng ta tạo nhiều comments một lúc khi bấm nút save
, do vậy mình sẽ không sử dụng comments_controller.rb
(bởi vì controller này hiểu rằng dùng cho việc CRUD
cho 1 comment.
Do vậy ở đây mình có thể sử dụng resource
thay vì dùng resources
. Ví dụ mình sử dụng một controller khác với resource như sau:
# routes.rb
resource :batch_comments
Về một số khác biệt giữa resource số ít và resources số nhiều mình có viết ở bài dưới đây.
Tại đây chúng ta sẽ gửi request lên controller vừa tạo là batch_comments_controler.rb
và xử lý tại controller này.
Tuy nhiên cũng có một giải pháp khác để dễ dàng bảo trì hơn khi chúng ta làm với dự án lớn đó là sử dụng services
. Khi này chúng ta sẽ không xử lý logic nhiều ở controller
nữa mà sẽ xử lý nó ở trong 1 service
và trả kết quả cho controller
.
Mình sẽ viết một bài về services
sau. Với phần này mình sẽ xử lý hết ở controller
.
Đầu tiên chúng ta tạo batch_comments_controller.rb
như sau:
rails g controller batch_comments
Đi tới xử lý tạo các comments tại controller này.
Sửa một chút về form_comment
như sau:
# _comment_form.slim
= simple_form_for post, url: batch_comments_path, method: :post do |f|
= f.hidden_field :id
...
= link_to "#!", class: "btn btn-w-m btn-primary", id: "js-comment-form-save-btn",
data: {disable_with: t(".submitting")} do
= t(".save")
Tại batch_comments.coffee
chúng ta xử lý sử dụng ajax
để submit form và xử lý sau khi comments được lưu như sau:
$ ->
$("#js-comment-form-save-btn").click ->
is_disabled = $("#js-comment-form-save-btn").attr "disabled"
return if is_disabled
id = $("#post_id").val()
params = $("#comments-form form").serialize()
submitting_text = $(@).data "disable-with"
submitted_text = $(@).html()
$("#js-comment-form-save-btn").attr "disabled", true
$("#js-comment-form-save-btn").html submitting_text
$.ajax
type: "POST"
dataType: "JSON"
url: "/batch_comments"
data: params
success: (data) ->
$("#js-comment-form-save-btn").html submitted_text
$("#js-comment-form-save-btn").removeAttr "disabled"
insert_comment_ids_to_forms data
error: () ->
$("#js-comment-form-save-btn").html submitted_text
$("#js-comment-form-save-btn").removeAttr "disabled"
return
insert_comment_ids_to_forms = (data) ->
$.each data, (key, value) ->
comment_field_comment_form = "<input value=\"#{value}\" name=\"post[comments_attributes][#{key}][id]\" type=\"hidden\">"
comment_field_basic_form = "<input value=\"#{value}\" name=\"post[comment_ids][]\" type=\"hidden\">"
$("#comments-form form").append comment_field_comment_form
$("#basic-information form").append comment_field_basic_form
Tiếp theo chúng ta xử lý trên server như sau:
# batch_comments_controller.rb
class BatchCommentsController < ApplicationController
def create
@result =
begin
if params[:id].present?
post = Post.find params[:id]
post.assign_attributes params
post.save!
{success: true}
else
ActiveRecord::Base.transaction do
result = {}
params[:comments_attributes]&.each do |key, value|
if value[:id].present?
Comment.find(value[:id]).update value
else
result[key] = Comment.create!(value).id
end
end
result
end
end
rescue StandardError
{success: false}
end
render nothing: true, status: 400 if @result == {success: false}
end
private
def post_params
params.require(:post).permit :title, :content, comment_ids: []
end
end
Cuối cùng mình tạo file builder trong view để render kết quả.
# views/batch_comments/create.json.jbuilder
json.merge!(@result)
Như vậy là đã xong phần submit form comment sử dụng ajax.
Cuối cùng để lưu bài post
chúng ta submit post_form
như thông thường. Nó sẽ tiến hành lưu bài post
cùng với các comments
mình đã tạo ở bên tab kia.
Cảm ơn các bạn đã xem bài viết.
All rights reserved