Davigor
Davigor

Reputation: 403

Passing custom params through from view to a form_with

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

Answers (1)

3limin4t0r
3limin4t0r

Reputation: 21160

Solution 1 - Clean Pass Through

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.

Solution 2 - Nested Resources

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 device
  • PUT /devices/:id for an existing device

Upvotes: 5

Related Questions