I'm using Simple Form here, but this is an issue with normal Rails forms, too. When using shallow routes, form_for needs different arguments depending in what context it's used.
Example: For editing (http://localhost:3000/notes/2/edit), _form.html.erb needs to have simple_form_for(@note). But for creating a new note (http://localhost:3000/customers/2/notes/new) _form.html.erb needs simple_form_for([@customer, @note]). If either receives the wrong arguments, I'll get a method not found error.
What's the best way to deal with this?
Are these my only options?
Example follows:
config/routes.rb
Billing::Application.routes.draw do
resources :customers, :shallow => true do
resources :notes
end
end
rake routes | grep note
customer_notes GET /customers/:customer_id/notes(.:format) notes#index
POST /customers/:customer_id/notes(.:format) notes#create
new_customer_note GET /customers/:customer_id/notes/new(.:format) notes#new
edit_note GET /notes/:id/edit(.:format) notes#edit
note GET /notes/:id(.:format) notes#show
PUT /notes/:id(.:format) notes#update
DELETE /notes/:id(.:format) notes#destroy
app/views/notes/_form.html.erb
# v----------------------------- Right here
<%= simple_form_for (@note), html: { class: 'form-vertical'} do |f| %>
<%= f.input :content %>
<%= f.button :submit %>
<% end -%>
app/views/notes/new.html.erb
<h1>New note</h1>
<%= render 'form' %>
<%= link_to 'Back', customer_path(@customer) %>
app/views/notes/edit.html.erb
<h1>Editing note</h1>
<%= render 'form' %>
<%= link_to 'Show', @note %>
<%= link_to 'Back', customer_path(@customer) %>
app/controllers/notes_controller.rb
class NotesController < ApplicationController
def show
@note = Note.find(params[:id])
@customer = Customer.find(@note.customer_id)
respond_to do |format|
format.html
format.json {render json: @note }
end
end
# GET /notes/new
# GET /notes/new.json
def new
@note = Note.new
@customer = Customer.find(params[:customer_id])
respond_to do |format|
format.html # new.html.erb
format.json { render json: @note }
end
end
# GET /notes/1/edit
def edit
@note = Note.find(params[:id])
@customer = Customer.find(@note.customer_id)
end
# POST /notes
# POST /notes.json
def create
@customer = Customer.find(params[:customer_id])
@note = @customer.notes.build(params[:note])
respond_to do |format|
if @note.save
format.html { redirect_to @customer, notice: 'Note was successfully created.' }
format.json { render json: @note, status: :created, location: @note }
else
format.html { render action: "new" }
format.json { render json: @note.errors, status: :unprocessable_entity }
end
end
end
# PUT /notes/1
# PUT /notes/1.json
def update
@note = Note.find(params[:id])
@customer = Customer.find(@note.customer_id)
respond_to do |format|
if @note.update_attributes(params[:note])
format.html { redirect_to @customer, notice: 'Note was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: @note.errors, status: :unprocessable_entity }
end
end
end
# DELETE /notes/1
# DELETE /notes/1.json
def destroy
@note = Note.find(params[:id])
@note.destroy
respond_to do |format|
format.html { redirect_to :back }
format.json { head :no_content }
end
end
end
If the first object in the array you pass the form builder is nil, Rails will POST to the second object only. For this reason simply don't set your @customer object in your controller's edit action. If you need access to the customer object, call it through @note.
If you're using the same partial for new and edit, you'll want to set @note.customer in the controller's new action (@customer won't be set when editing).
I think this is how the Rails team intended it to work.
I'd like to offer a slight modification to James' solution:
# app/helpers/application_helper.rb
def shallow_args(parent, child)
child.try(:new_record?) ? [parent, child] : child
end
Instead of relying on the controller action being called "new" -- though it likely will be 95% of the time -- this just checks if the child is a new record.
Here's what I came up with:
app/helpers/application_helper.rb
module ApplicationHelper
# Public: Pick the correct arguments for form_for when shallow routes
# are used.
#
# parent - The Resource that has_* child
# child - The Resource that belongs_to parent.
def shallow_args(parent, child)
params[:action] == 'new' ? [parent, child] : child
end
end
app/views/notes/_form.html.erb
<%= simple_form_for shallow_args(@customer, @note), html: { class: 'form-vertical'} do |f| %>
<%= f.input :content %>
<%= f.button :submit %>
<% end -%>
I don't know that it's the best solution, but it seems to work alright.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With