I wrote it very carefully, so it has a lot of volume. Skip what you know: thumbs up:
-I want to operate multiple models without using accepts_nested_attributes_for
.
・ I want to update not only new data.
A method that allows you to update the records of the associated model at once.
Example) When sending a message with multiple images attached
In the above case, you can save the message and the image associated with it. It's convenient. However, ** Actually, this method is not very popular **: joy: (Click here for details)
Therefore, I will introduce alternative form objects.
A separate class for processing forms, separated from the model.
It allows you to update multiple records without ** accepts_nested_attributes_for
. ** **
If you want to know more about other merits and principles, please see this article. The explanation is insanely easy to understand: pray:
Implement a function that allows the user (parent) to associate a message (child) with an image (grandchild) and send it.
Change the parent, child, and grandchild models to suit your situation.
-Routing settings ・ Carrier wave settings required for image posting ・ Creating a table for each model
Let's take a look!
user.rb
class User < ApplicationRecord
has_many :messages
end
message.rb
class Message < ApplicationRecord
has_many :pictures
belongs_to :user
#The validation of the form written here moves to the form object.
end
picture.rb
class Picture < ApplicationRecord
belongs_to :message
#Those who do not post images do not need the following description.
mount_uploader :picture, PictureUploader
end
There is no description about validation. It's refreshing and easy to read: relaxed:
I would like you to pay attention to only the following two points.
-Update the information of @ message
based on the value passed from the parameter in assigns_attributes
.
-MessageForm
is a class for form objects, which will be explained later.
messages_controller.rb
#New creation screen
def new
@message = MessageForm.new
end
#Create New
def create
@message = MessageForm.new
@message.assign_attributes(message_form_params)
if @message.save
#Success / failure processing
end
end
#Editing screen
def edit
# params[:id]For the part, enter a value that can identify the object to be edited.
@message = MessageForm.new(message = Message.find(params[:id]))
end
#Edit
def update
@message = MessageForm.new(message = Message.find(params[:id]))
@message.assign_attributes(message_form_params)
if @message.save
#Success / failure processing
end
end
private
#Strong parameters
def message_form_params
params.require(:message_form).permit(:body, pictures_attributes: [:picture]).merge(user_id: current_user)
end
The strong parameter pictures_attributes
will be explained next.
This is the new message creation screen.
ruby:new.html.erb
<%= form_with model: @message, url:Path for message composition, local: true do |f| %>
<%= f.text_area :body %>
<%= f.fields_for :pictures do |picture| %>
<%= picture.file_field :picture, multiple: "multiple", name: "message_form[pictures_attributes][][picture]" %>
<% end %>
<%= f.submit "Send" %>
<% end %>
The explanation will focus on the parameters.
First of all, suppose you typed "OK" in the text area and sent it. The parameters at this time are as follows. (Please check at the terminal.)
Parameters: {"authenticity_token"=>"Abbreviation", "message_form"=>{"body"=>"OK"}, "button"=>""}
It contains the value you entered in the body column.
Here, it is "message_form "
because @ message
is generated from the MessageForm
class. (This will happen automatically.)
Next, if you want to add image information to this, you want to make it look like the following.
Parameters: {"authenticity_token"=>"Abbreviation", "message_form"=>{Image information, "body"=>"OK"}, "button"=>""}
So, let's use the name attribute of file_field
.
name: "message_form[pictures_attributes][][picture]"
If you write like this, the parameters when posting two images are as follows. (The image information is long, so it is omitted.)
Parameters: {"authenticity_token"=>"Abbreviation", "message_form"=>{"pictures_attributes"=>[{"picture"=>1st image information}, {"picture"=>2nd image information}], "body"=>"OK"}, "button"=>""}
You can see that the image information is included under " message_form "
.
Also, " pictures_attributes "=> [{"picture "=>
corresponds to the controller's strong parameters.
For now, it looks like you can pass the value to the controller: relaxed:
** By the way, you need to be careful in the case of the edit screen. ** ** The reason is written in this article, so please refer to it if you have time.
This is the most complicated part. I will explain it in four parts, so please read it comfortably: ok_hand:
message_form.rb
class MessageForm
# part-1
include ActiveModel::Model
include Virtus.model
extend CarrierWave::Mount
validates :body, presence: true
attribute :body, String
attribute :user_id, Integer
mount_uploader :picture, PictureUploader
attr_accessor :pictures
# part-2
def initialize(message = Message.new)
@message = message
self.attributes = @message.attributes if @message.persisted?
end
# part-3
def assign_attributes(params = {})
@params = params
pictures_attributes = params[:pictures_attributes]
@pictures ||= []
pictures_attributes&.map do |pictures_attribute|
picture = Picture.new(pictures_attribute)
@pictures.push(picture)
end
@params.delete(:pictures_attributes)
@message.assign_attributes(@params) if @message.persisted?
super(@params)
end
# part-4
def save
return false if invalid?
if @message.persisted?
@message.pictures = pictures if pictures.present?
@message.save!
else
message = Message.new(user_id: user_id,
body: body)
message.pictures = pictures if pictures.present?
message.save!
end
end
end
python
include ActiveModel::Model
include Virtus.model
extend CarrierWave::Mount
validates :body, presence: true
attribute :body, String
attribute :user_id, Integer
mount_uploader :picture, PictureUploader
attr_accessor :pictures
This is a collection of included modules.
-ActiveModel :: Model
allows validation to be used when saving a MessageForm
object. The validates
part. It's convenient to be able to validate without inheriting ActiveRecord
.
-Virtus.model
defines the attribute name and type of this class. The attribute
part.
It specifies what to receive as a parameter. By the way, I have installed the gem virtus.
-CarrierWave :: Mount
is for uploading images. The part of mount_uploader
.
attr_accessor
is for setting image information in the message. (Used in part-4.)
Give the object a property.
python
def initialize(message = Message.new)
@message = message
self.attributes = @message.attributes if @message.persisted?
end
The point here is the argument of initialize
.
If the controller's new
method has an argument, it is receiving it.
python
def edit
@message = MessageForm.new(message = Message.find(params[:id]))
end
In other words, when the object is created with new
, if there is information of the message already created, the attributes of the form object are rewritten based on it.
('self.attributes` part)
I'm sorry for those who are getting tired. A little more patience. .. ..
python
def assign_attributes(params = {})
@params = params
pictures_attributes = params[:pictures_attributes]
#If you write pictures, you can call up the image information.
@pictures ||= []
pictures_attributes&.map do |pictures_attribute|
picture = Picture.new(pictures_attribute)
@pictures.push(picture)
end
#Remove image information from parameters
@params.delete(:pictures_attributes)
#Update the information of the message you have already created
@message.assign_attributes(@params) if @message.persisted?
#Update form object information
super(@params)
end
Here, assign_attributes
sets the parameter value to @ message
.
The assign_attributes
method is originally a method for updating data,
-I want to reflect the editing information of the form object in the already created message.
・ I want to be able to get or set image information by writing pictures
(attr_accessor
in part-2)
I've modified it a bit for that reason: sunglasses:
Finally the last! It is a conversion work to the Message
model.
python
def save
#Validate before storage
return false if invalid?
#Processing when editing
if @message.persisted?
@message.pictures = pictures if pictures.present?
@message.save!
else
#Processing in case of new creation
message = Message.new(user_id: user_id,
body: body)
message.pictures = pictures if pictures.present?
message.save!
end
end
In the case of editing, it is no different from normal saving.
The @ message
at this time is the same as the message
in part-2.
The important thing is to convert to the Message
object below.
python
message = Message.new(user_id: user_id,
body: body)
I'm passing the value that the form object brought to the column.
This completes the conversion from the MessageForm
to the Message
object.
python
message.pictures = pictures if pictures.present?
Finally, set the image information in the message and finish. Thank you for your hard work: clap:
ruby: 2.7.1 rails: 6.0.3.3