views:

40

answers:

1

I have a complex form for a model Schedule that belongs to a parent Listing model:

class Listing < ActiveRecord::Base
  has_many :schedules, :dependent => :destroy
  accepts_nested_attributes_for :schedules, :allow_destroy => true, :reject_if => :all_blank
  ...
end

class Schedule < ActiveRecord::Base
  belongs_to :listing
  ...
end

Because of the complexity of the schedule form I do not want to display it for each item in the fields_for Schedules, so these only contain a title and a delete link (and a bunch of hidden fields for the other params).

Instead the complex form is used to asynchronously generate new form fields for each Schedule which are then inserted into the parent's form. This is simply the response from the Schedule form being posted (using a partial _new_schedule_fields) which is appended to a fields_for(:schedules) section in the Listing form. The child Schedule(s) then get created automatically when the Listing is saved since it accepts_attributes_for Schedules. The call to the Schedule form looks like this (sits after the form_for Listings in the page):

form_for([@listing, @schedule], :remote => true) do |f| 

This works very well when editing an existing Listing but the form_for used to build the complex Schedule form falls over on new Listings:

 => No route matches {:action=>"create", :controller=>"schedules")

In my routes I have (Rails 3):

resources :listings do
  resources :schedules
end

This makes perfect sense of course, as the expected route would be /listings/:listing_id/schedules/new but I wonder if there is a way to use the form_for builder to generate a form for a nested model belonging to an unsaved parent? /listings/new/schedules/new seems absurd but perfectly sums up what I'm after!

Grateful for any helpful suggestions.

Edit:

I should add that I'm building new Schedule instances in the new and edit methods of the Listings controller, like so:

def new
  @listing = Listing.new
  @schedule = @listing.schedules.build
end

def edit
  @listing = Listing.find(params[:id])
  @schedule = @listing.schedules.build
end
+1  A: 

You can't use form_for by default to do an embedded object under a new parent, because there is no way for the controller to return an object that is the child of your new object, since you cannot assign the child to an object with no id. You would have to set up a custom route. Conceptually to me, though, the limitation makes sense - you're NOT requesting the application to return a child of your new object, because you can't make a child until you make the object, so you'd call the non-embedded route, just form_for(@schedule). Then, just return the whole build tree as one object, so that it all saves together. You have to embed it in the final save request, until that time, there IS no embedded object.

jasonpgignac
Yep, that makes sense. The downside is that I have to have a route for Schedules from the root, although they should not be directly accessible as a schedule always belongs to a listing. I've made the change though and it seems to work. But now I have a problem with the create method for listings when the listing doesn't have any schedules. I've had to do: unless @schedule @schedule = @listing.schedules.build- but this leads to an empty schedule being inserted instead.
Ola Tuvesson
You could always just store the new schedule form inside of a javascript function right on the new listing page, then you wouldn't have to call back at all, I suppose, thus getting rid of the confusing routes. As for your new issue, let me understand the logic here - is a listing required to have one or more schedules, then? What is the purpose of the build line?
jasonpgignac
The purpose of the schedule.build is to overcome the error message I get when the form is submitted with no schedules and fails to pass my validations. The listings controller seems to trigger the create function and complains that schedule is a nil class without it. New and edit methods already have the schedule.build for the same reason. A listing can have any number of schedules, including none.
Ola Tuvesson
As for the prebuilt form solution, that certainly works. I did at one point (out of desperation) just stick a flat HTML rendering of the form in, which worked well. But that way of doing things seems completely off the rails to me!
Ola Tuvesson
I agree it would be nice to have a more elegant solution, and it feels 'less than railsy'. What I have done is to make the form a partial inside the schedules views, and then call it from my larger page inside the listings views - if that makes sense. Then it's really no different than what you were saying, except I'm retrieving the partial beforehand instead of as an ajax request.
jasonpgignac
As for the schedule.build issue, what *I* would do, is to look at it this way: either 1) You want the default behaviour of the listing model to be that it creates a schedule if there are none present, in which case you'd drop the validation and make a before filter, or 2) You want the default behaviour of the controller to be to insert a schedule if none are present in the specific situation, in which case I'd add a line to test the data before applying it, and just add an empty (or default) schedule into the data set before applying it to the actual object in the save. Does that make sense?
jasonpgignac