44

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?

  • I could make two separate forms, but that seems messy.
  • I have to set @customer for the back link, but I could use a different variable in the form (say, @customer_form) and just not set it in the edit and update methods, but that's inconsistent and slightly confusing, since I'd have to set both @customer_form and @customer in the new method.
  • I could do what this guy did and split the form up across multiple files. It looks like the best option so far, but I don't really like it much, since you can't just open _form.html.erb and see what's happening.

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
James
  • 2,626
  • 5
  • 37
  • 51
  • See Eric's solution below. The solution is as simple as not defining `@customer` in your edit action. – Andrew Jul 10 '14 at 05:35

3 Answers3

39

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.

Eric Boehs
  • 1,327
  • 13
  • 17
  • Sometimes the solution is so simple it's not obvious. – Andrew Jul 10 '14 at 05:31
  • but how could you do @note.customer?? I tried to access the parent object(@customer) from the child(@note) using @note.customer, but it would only return nil class... – Sardonic Feb 10 '15 at 19:32
  • 1
    @Sardonic In the `_form` you'll need to do `form_for [@customer, @note]` then in `NotesController` set `@customer` in your `new` action but not `edit`. If you need access to customer you can use `@note.customer || @customer`. – Eric Boehs Feb 11 '15 at 22:48
  • 1
    @Sardonic `@note.customer` will be `nil` for your `new` action. You'll need to find and set `@customer` based on `params[:customer_id]`. You may want to do something like `@note = @customer.notes.new` so that `@note.customer` is set. If your in the `edit` action and it's `nil`, something else is wrong that's outside of the scope of this question (like your associations or table data is messed up). – Eric Boehs Feb 13 '15 at 00:13
  • This also works with namespaces, e.g. `[:admin, @customer, @note]` where `@customer` is set on the new page but nil on the edit page. – nruth Oct 07 '15 at 01:03
  • This works for me, however I can't seem to test this, since this causes a failure in my tests due to the value being nil. – android_student Mar 09 '16 at 05:03
26

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.

imderek
  • 1,306
  • 13
  • 22
10

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.

James
  • 2,626
  • 5
  • 37
  • 51
  • 4
    This looks like a decent solution, but I truly wish Rails had something baked in to handle this. After all, anyone using shallow routes must certainly encounter this issue as soon as they create their first form, right? – imderek Mar 29 '12 at 21:20
  • 1
    @imderek See Eric's solution below. There's no need for a custom helper method. – Andrew Jul 10 '14 at 05:32
  • 1
    One advantage this solution has over Eric's is lower coupling with the controller. With this solution, you are free to assign whatever instance variables you like. – Jared Beck Mar 04 '16 at 19:56