Build form object với gem reform trong Rails
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ó ...
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(" ", "")} 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