6

So, imagine I have this model:

class Car
  has_one :engine
end

and the engine model:

class Engine
  belongs_to :car
end

When I present the form for the user, so that he can create a new car, I only want to allow him to select from one of the available engines ( the available engines will be in a select, populated by the collection_select ). The thing is, if I build the field like this:

<%= f.collection_select :engine,Engine.all,:id,:name %>

When I will try to save it, I will get an AssociationTypeMismatch saying that it expected an Engine, but it received a string.

Is this the way to do it?

def create
  car = Car.new(params[:car])
  engine = Engine.find(params[:engine])
  car.engine = engine
  if car.save
     # redirect somewhere
  else
     # do something with the errors
  end
end

I always felt that stuff, like associating an engine to a car, are done automatically by Rails, but I don't know how to make him do it.

Is switching the has_one and belongs_to associations the only way to achieve this?

I am lost and I feel like I'm missing something very basic here.

Geo
  • 93,257
  • 117
  • 344
  • 520

3 Answers3

10

You should use engine_id

<%= f.collection_select :engine_id, Engine.all, :id, :name %>

UPD

as far as Engine is not belongs_to Car so you shoulduse Nested Attributes here. This screencast will be very useful for you:

Checkout api: http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods/accepts_nested_attributes_for

Short intro:

class Car
  has_one :engine
  accepts_nested_attributes_for :engine
end

and in your form:

<%= form_for @car ... do |f| %>
  ...
  <%= f.fields_for :engine do |b| %>
    <%= b.collection_select :id, Engine.all, :id, :name %>
    ...
  <% end %>
  ...
<% end %>
fl00r
  • 82,987
  • 33
  • 217
  • 237
  • So, even though the Engine is the one holding the association, I need to have an `engine_id` field in the Car model? – Geo Apr 06 '11 at 20:45
  • No! :) You should use `accepts_nested_attributes_for` here. I'll update my answer – fl00r Apr 06 '11 at 20:46
  • And when I save the Car, I will also have access to the associated engine, using the `.engine` method,right? – Geo Apr 06 '11 at 20:54
  • But how do I populate the engines from a `collection_select`? Do I have to write `:engine_id` in the `fields_for`? Or just `id`? – Geo Apr 06 '11 at 21:07
  • I still have one small question. If I leave the view to be like `form_for @car`, then none of the fields in the `fields_for` are displaying. If I change the top-level form to `form_for :car`, they are displayed. Why is that? – Geo Apr 06 '11 at 21:26
  • 2
    fields for is looking for exist engines, so if you haven't got any for @car - nothing will appear, you should initialize it. Like `f.fields_for :engine, @car.build_ingine do |f| ...` – fl00r Apr 06 '11 at 21:29
  • Thank you very much for all the patience and responses! – Geo Apr 06 '11 at 21:33
2

I faced the same problem and here is my solution (improvements based on the fl00r answer):

  1. Do everything as fl00r have said.

  2. Add optional: true to the class description

class Engine belongs_to :car, optional: true end

More info at: http://blog.bigbinary.com/2016/02/15/rails-5-makes-belong-to-association-required-by-default.html

  1. Modify
<%= f.fields_for :engine do |b| %>
  <%= b.collection_select :id, Engine.all, :id, :name %>
  ... 
<% end %>

to

<%= f.fields_for :engine, :include_id => false do |b| %>
  <%= b.collection_select :id, Engine.all, :id, :name %>
  ...
<% end %>

More info at: Stop rails from generating hidden field for fields_for method

  1. Modify your EngineController

3.1. Modify

def car_params
  params.require(:car).permit(:name)
end

to

def car_params
  params.require(:car).permit(:name, engine_attributes: [:id, :name])
end

More info at: Rails 4 Nested Attributes Unpermitted Parameters

3.1. Add a function (private)

def set_engine

  engine_id = car_params[:engine_attributes][:id]

  @engine = Engine.find(engine_id)

  @car.engine = @engine

  @car.save
end

3.2. Modify EngineController#update

  def update
    respond_to do |format|

      if @car.update(car_params)
        format.html { redirect_to @car, notice: 'Car was successfully updated.' }
        format.json { render :show, status: :ok, location: @car }
      else
        format.html { render :edit }
        format.json { render json: @car.errors, status: :unprocessable_entity }
      end
    end
  end

to

  def update
    respond_to do |format|

      if set_engine && @car.update(car_params)
        format.html { redirect_to @car, notice: 'Car was successfully updated.' }
        format.json { render :show, status: :ok, location: @car }
      else
        format.html { render :edit }
        format.json { render json: @car.errors, status: :unprocessable_entity }
      end
    end
  end
Community
  • 1
  • 1
prograils
  • 2,248
  • 1
  • 28
  • 45
0

Response to previous answer from Kyle--I like this simple approach however I had to change it as follows to work for my app (I'm in Rails 3.2.13). The main point was that the map needs to return an Array--so I think it was perhaps a typo putting two arrays separated by a comma. I also changed the string reference to the id attribute to a symbol but I'm not sure if this is essential or just another option. At an rate, here is what worked for me.

<%= f.select :class_location_id, ClassLocation.all.map {|e| [e.place, e.id]} %>
Sedona
  • 141
  • 1
  • 10