Reputation: 403
New to rails and trying to figure out how to pass params through from a view to a form and then on to a controller, or most likely, find a better way of assigning foreign keys for a new object. I have a button that is linked to specific outlet object, and this button allows the user to create a new device object associated with that outlet. There is a foreign key on my device table that points to an outlet id.
<%= button_to 'Add Device', new_device_path, :method => :get, params: {:oulet_id => outlet.id} %>
The new_device_path
then goes through new.html.erb
:
<h1>Add New Device to Outlet</h1>
<%= render 'form' %>
<%= button_to 'Back', grows_path, :method => :get %>
and then onward to _form.html.erb:
<%= form_with model: @device, local: true do |form| %>
<% if @device.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(device.errors.count, "error") %> prohibited this device from being saved:</h2>
<ul>
<% @device.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= form.label :title %><br />
<%= form.text_field :title %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
The problem is that new.html.erb
receives my params[:oulet_id]
, but not the form, so I can't pass them on to the controller where I can set the oulet_id
entry on the new device to the specific outlet.
Previously, I've been able to pass custom params directly from a form and handle them on the controllers and accomplish what I'm asking. In this case, however, I feel like trying to pass the params forward through multiple pages like this is not the right approach... how am I supposed to accomplish this?
Upvotes: 4
Views: 4403
Reputation: 21160
This is the simple solution that requires the least amount of change in your current application.
You can set the attribute in the #new method of the DevicesController and pass it to the form as an hidden field like so:
class DevicesController < ApplicationController
def new
@device = Device.new(outlet_id: params[:outlet_id])
end
end
Now add a hidden field to the form. This is necessary to preserve and send the outlet_id
to the form path (in this case POST /devices/:id
). I leave out the error handling part for simplicity.
<%= form_with model: @device, local: true do |form| %>
<%= form.hidden_field :outlet_id %>
<p>
<%= form.label :title %><br />
<%= form.text_field :title %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
Don't forget to whitelist the new param in your DevicesController#device_params method, otherwise it won't save.
A better approach would be to use Nested Resources, but that would require a structural change. Firstly add a nested route in your routes.rb
file:
resources :outlets do
resources :devices, shallow: true
end
This will create the following routes:
+-----------+---------------------------------+-------------------+------------------------+
| HTTP Verb | Path | Controller#Action | Named Helper |
|-----------|---------------------------------|-------------------|------------------------|
| GET | /outlets | outlets#index | outlets_path |
| GET | /outlets/:id | outlets#show | outlet_path |
| GET | /outlets/new | outlets#new | new_outlet_path |
| GET | /outlets/:id/edit | outlets#edit | edit_outlet_path |
| POST | /outlets | outlets#create | outlets_path |
| PATCH/PUT | /outlets/:id | outlets#update | outlet_path |
| DELETE | /outlets/:id | outlets#destroy | outlet_path |
| GET | /outlets/:outlet_id/devices | devices#index | outlet_devices_path |
| GET | /devices/:id | devices#show | device_path |
| GET | /outlets/:outlet_id/devices/new | devices#new | new_outlet_device_path |
| GET | /devices/:id/edit | devices#edit | edit_device_path |
| POST | /outlets/:outlet_id/devices | devices#create | outlet_devices_path |
| PATCH/PUT | /devices/:id | devices#update | device_path |
| DELETE | /devices/:id | devices#destroy | device_path |
+-----------+---------------------------------+-------------------+------------------------+
Now add a before_action
callback in the controller alongside a new way of building the new instance for #new
and #create
:
class DevicesController < ApplicationController
before_action :set_device, on: %i[show edit update delete] # already present
before_action :set_outlet, on: %i[index new create] # new
def new
# for outlet has_many devices
@device = @outlet.devices.build
# for outlet has_one device
@device = @outlet.build_device
end
def create
# for outlet has_many devices
@device = @outlet.devices.build(device_params)
# for outlet has_one device
@device = @outlet.build_device(device_params)
# followed by your normal create stuff
end
# code...
private
def set_devise # already present
@device = Device.find(params[:id])
end
def set_outlet # new
@outlet = Outlet.find(params[:outlet_id])
end
# code ...
end
Now remove the params param and change the path of your of your button to:
<%= button_to 'Add Device', new_outlet_device_path(outlet), method: :get %>
Now the last thing to do is change the url of the form so it points to the right path for #create and #update. For this, change the form creation line to:
<%= form_with model: [@outlet, @device].compact, local: true do |form| %>
Passing an array to a form helper in Rails normally sets up nested paths. I call #compact to ensure that the form works fine when editing. Because when you edit a device @outlet
is not set resulting in [nil, #<Device...>]
. When calling #compact the nil
values are removed from the array.
This will result in the following requests:
POST /outlets/:outlet_id/devices
for a new devicePUT /devices/:id
for an existing deviceUpvotes: 5