Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When using shallow routes, different routes require different form_for arguments

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
like image 562
James Avatar asked Mar 19 '12 15:03

James


3 Answers

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.

like image 110
Eric Boehs Avatar answered Nov 03 '22 14:11

Eric Boehs


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.

like image 26
imderek Avatar answered Nov 03 '22 14:11

imderek


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.

like image 10
James Avatar answered Nov 03 '22 14:11

James