I have a form containing a select and a text field (you can pick a MediaType
or create one):
<div>
<%= form.label :media_type_id, style: "display: block" %>
<%= form.collection_select :media_type_id, MediaType.all, :id, :name, :prompt => "Select a Media Type" %>
or create one:
<%= form.text_field :new_media_type_name %>
</div>
The model code contains stuff to support the new_media_type_name
:
class Medium < ApplicationRecord
has_many :authorships
has_many :authors, through: :authorships
belongs_to :book_genre
belongs_to :media_type
attr_accessor :new_media_type_name
before_save :create_media_type_from_name
def create_media_type_from_name
create_media_type(:name => new_media_type_name) unless new_media_type_name.blank?
end
end
This all works correctly in the console:
irb(main):001> m = Medium.new
=>
#<Medium:0x00007f9c867307d8
...
irb(main):002> m.create_media_type(:name => "Test")
TRANSACTION (1.1ms) BEGIN
MediaType Create (2.1ms) INSERT INTO "media_types" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "Test"], ["created_at", "2024-09-29 01:06:50.826184"], ["updated_at", "2024-09-29 01:06:50.826184"]]
TRANSACTION (17.3ms) COMMIT
=>
#<MediaType:0x00007f9c86cb6270
id: 6,
name: "Test",
created_at: Sun, 29 Sep 2024 01:06:50.826184000 UTC +00:00,
updated_at: Sun, 29 Sep 2024 01:06:50.826184000 UTC +00:00>
But running the app from the browser does not save the new MediaType
.
This is what I see in the output running rails s
:
Started POST "/media" for 192.168.1.142 at 2024-09-28 17:34:33 -0500
Processing by MediaController#create as HTML
Parameters: {"authenticity_token"=>"[FILTERED]", "medium"=>{"title"=>"The New Test", "country"=>"Canada", "published_on"=>"2024-09-05", "media_type_id"=>"", "new_media_type_name"=>"Spaceship", "book_genre_id"=>"3", "summary"=>"The summary", "cover_art_path"=>"", "isbn"=>""}, "commit"=>"Create Medium"}
TRANSACTION (0.8ms) BEGIN
↳ app/controllers/media_controller.rb:26:in `create'
BookGenre Load (1.2ms) SELECT "book_genres".* FROM "book_genres" WHERE "book_genres"."id" = $1 LIMIT $2 [["id", 3], ["LIMIT", 1]]
↳ app/controllers/media_controller.rb:26:in `create'
TRANSACTION (0.9ms) ROLLBACK
↳ app/controllers/media_controller.rb:26:in `create'
Rendering layout layouts/application.html.erb
Rendering media/new.html.erb within layouts/application
MediaType Load (1.3ms) SELECT "media_types".* FROM "media_types"
↳ app/views/media/_form.html.erb:35
BookGenre Load (1.3ms) SELECT "book_genres".* FROM "book_genres" ORDER BY "book_genres"."name" ASC
↳ app/views/media/_form.html.erb:42:in `map'
Rendered media/_form.html.erb (Duration: 48.9ms | Allocations: 8316)
Rendered media/new.html.erb within layouts/application (Duration: 50.5ms | Allocations: 8853)
Rendered layout layouts/application.html.erb (Duration: 85.5ms | Allocations: 20627)
Completed 422 Unprocessable Entity in 181ms (Views: 85.5ms | ActiveRecord: 31.2ms | Allocations: 44624)
The problem is, the save fails and the error displayed in the browser is always "Media type must exist". I think this means it cannot create the Medium
unless the media_type
attached to it already exists. But I thought that was the purpose of the before_save
block in the model.
Please, I beg you rails gods and godesses, what am I missing? Ryan? Ryan Bates, are you there??
Update: I discovered if I select a media_type
in the form while entering a new_media_type_name
at the same time, it creates the new media_type
and sets the new media_type
as the media_type
reference in the new Medium object.
To create nested records in Rails you use accepts_nested_attributes
to create a setter in the model:
class Medium < ApplicationRecord
# ...
belongs_to :media_type
accepts_nested_attributes_for :media_type,
reject_if: :all_blank
end
To create the inputs in the form you use fields_for:
<div>
<div class="field">
<%= form.label :media_type_id, style: "display: block" %>
<%= form.collection_select :media_type_id, MediaType.all, :id, :name, prompt: "Select a Media Type" %>
</div>
<fieldset>
<legend>Or create one</legend>
<%= form.fields_for(:media_type) do |media_type_form| %>
<div class="field">
<%= media_type_form.label :name %>
<%= media_type_form.text_field :name %>
</div>
<% end %>
</fieldset>
</div>
For the fields to appear you need to "seed" the assocation with @media.build_media_type
in the controller.
You then whitelist the nested attributes in your controller by passing a hash as the last argument (the method predates real keyword args):
def medium_attributes
params.require(:medium)
.permit(
:foo,
:bar,
:baz,
media_type_attributes: [:name]
)
end
The other option to handle this if the other record can be created separately is to use AJAX to send a request to create the other record ansyncronously but that's really a topic for another day.