There are tables with various child elements such as @ comment
and @ employee
associated with @ shop
. At the time of initial information registration, all child elements such as @ shop
, @ comment
, and @employee
could be saved together with the parent element.
Although these were achieved using ʻaccepts_nested_attributes_for` in the form of the article below,
▼ It was realized like this Create child table data at once with fields_for (I also write a test) [Rails] [Rspec]
Of these, editing forms only for @ shop
and posting / editing forms such as @ comment
and @employee
became necessary, so the model
gradually became bloated with various descriptions.
↑ In the above situation, form_object
can simplify the description of the model by collecting validation and default value settings related to a specific form in one place. Personally, I've stumbled upon the introduction, so I'll write an article and keep a record.
The execution environment is as follows.
The basic way to write form_object
, controller
, and view
is as follows. This time, I would like to take an example of a form that allows you to register one @ comment
when you first register @ shop
.
This article was the most helpful for implementation.
Save multiple child records without accepts_nested_attributes_for
shops | |
---|---|
name | string |
category | integer |
↑ category is the shop type. ʻEnum` column.
comments | |
---|---|
content | text |
shop_id | integer |
form_object
/forms/shop_entry_form.rb
class ShopEntryForm
include ActiveModel::Model
# @Description about shop-----------------------------
concerning :ShopBuilder do
def initialize(params = {})
super(params)
@category = params[:category]
end
def facility
@shop ||= Shop.new
end
end
attr_accessor :name, :category
validates :name, presence: true
validates :category, presence: true
# @Description about comment-----------------------------
concerning :CommentBuilder do
attr_reader :comments_attributes
def comments
@comments_attributes ||= Comment.new
end
def comments_attributes=(attributes)
@comments_attributes = Comment.new(attributes)
end
end
attr_accessor :content
#Implementation logic------------------------------------
def save
#If a validation error occurs, false is returned and the following processing is not performed.
return false if invalid?
shop.assign_attributes(shop_params)
build_asscociation
shop.save ? true : false
end
private
def shop_params
{
name: name,
category: @category,
}
end
def build_asscociations
#Add comment to the child element of shop. However, if the contents are empty, it will not be added.
shop.comments << comments if comments[:content].present?
end
end
There was a lot of stumbling block with this alone. .. .. ..
First, the concerning: ShopBuilder do ... end
part has the following meanings.
#This description is...
concern :ShopBuilder do
...
end
#Same as below
module ShopBuilder
extend ActiveSupport::Concern
...
end
For details, see This article that I referred to when implementing.
Next, the part of ʻinitialize (params = {}) ... end` has the following meaning.
def initialize(params = {})
# @Make shop params accessible
super(params)
#Description for columns for which default values are set in DB
@category = params[:category]
end
First of all, regarding super (params)
, according to the following article, which was also a very helpful article for implementation.
A description that stores parameters with super (params)
, and has the same meaning as the following description.
@attributes = self.class._default_attributes.deep_dup
assign_attributes(params)
In addition, columns for which default values are set on the db side cannot access params unless you explicitly write to access params as shown below, and even if you enter a ** value, it will be the default value of DB. It has become ... ** **
@category = params[:category]
This mystery cannot be solved. I would like to make it a future issue. .. .. The reason why the default value is required on the db side for columns using enum is [this article](https://framgia.com/journal/rails%E3%81%AEenum-%E8%AB%B8%E5% Please see 88% 83% E3% 81% AE% E5% 89% A3% EF% BC% 88% EF% BC% 91% EF% BC% 89-% E3% 80% 80from-viblo /).
And the part of def comments_attributes = (attributes) ... end
,
def comments_attributes=(attributes)
@comments_attributes = Comment.new(attributes)
end
This is a way of writing setter method
that you rarely see if you are doing only Rails, and you can change the element with @ by ** argument in the form of method (argument)ending with
= I will.
Personally, I think it's close to the image of doing something like this.
def comments_attributes=(attributes) # ...The following is omitted
#Such an image
comments_attributes = attributes
#So you can call it like this
self.comments_attributes
# =>Contents of attributes
It's because I didn't understand Ruby getters and setters correctly. .. .. .. Tohoho. .. .. I will do my best. .. ..
For the method ending with =, see ["Introduction to Ruby for professionals, from language specifications to test-driven development / debugging techniques "](https://www.amazon.co.jp/dp/B077Q8BXHC). I reread
p215` of /) about 15 times.
controller
The following is a description of the controller. The controller looks like this.
app/controllers/shops_controller.rb
class ShopsController < ApplicationController
def new
@shop = ShopEntryForm.new
end
def create
@shop = ShopEntryForm.new(shop_entry_params)
if @shop.save
#What to do when it succeeds
else
#What to do if it fails
end
end
private
def shop_entry_params
params.require(:shop_entry_form).permit(:caregory,
:name,
comments_attributes: [:content])
end
end
I have the impression that the description did not decrease unexpectedly. At first, I expected that shop_entry_params
would be reduced from the controller, but I couldn't delete it from the controller after all. Only the method that creates the association could be removed from the controller.
As for Model, ** validation and default value setting methods, associations, etc. were all deleted! ** No additional description! !! After all, form_object
is a convenient way to write a slim model! !!
View Finally, the View looks like this.
haml:app/views/shops/new.html.haml
= form_with model: @shop, url: shops_path, local: true do |f|
= f.text_field :name
= f.fields_for :shop_comments, local: true do |comment_form|
= comment_form.text_field
= f.submit "Send"
Using fields_for
is no different from the implementation using ʻaccept_nested_attributes_for` ^^
The test was also very simple!
spec/forms/shop_entry_form_spec.rb
require 'rails_helper'
RSpec.describe ShopEntryForm, type: :model do
before do
@shop_form = ShopEntryForm.new(category: "category1", name: "Test shop")
end
describe "Validation test" do
it "Pass validation if you have a name and category" do
@shop_form.valid?
expect(@shop_form).to be_valid
end
#The following is omitted
end
end
All I had to do was pay attention to the location of the file, the RSpec.describe ShopEntryForm ...
part, and the description when creating the instance for testing ^^
This is a little old, but I created it with reference to this article.
Write a form object test in RSpec
Well, it took a really long time to implement. The actual form had three types of nested child elements, and the shape was quite complicated, but most of all, I think it was because I wasn't used to writing plain Ruby. .. .. Once calmed down, I would like to review Ruby again.
This is a summary of the articles and materials that I referred to this time.
▼ Overall writing Save multiple child records without accepts_nested_attributes_for
▼ How to access params Use form class "Introduction to Ruby for professionals, from language specifications to test-driven development and debugging techniques" (p.215)
▼ About Concerning Bite-sized separation of concerns
▼ How to write a test Write a form object test in RSpec
After this, the edit and update forms also remain, so I would like to work on that next ^ ^
Recommended Posts