Search code examples
ruby-on-railsformsnested-formsformtasticnested-attributes

Rails Formtastic nested form - form for attributes from join model


The problem I am facing with Formtastic is that I have a Form to create a new Order. In this form I want to select multiple existing Food items from a list. These should be added to the new Order I am submitting. At the same time I also want to set attributes in the FoodOrder join model. This Model has an integer quantity attribute for which i would like to have a field in my form.

What I am basically looking for is a form that lists all Food Items and puts a field for the quantity on the same line as the Food Item it belongs to.

The Models

class Order < ActiveRecord::Base
  belongs_to :user
  belongs_to :restaurant
  has_many :food_orders
  has_many :foods, :through => :food_orders
end

class FoodOrder < ActiveRecord::Base
  belongs_to :food
  belongs_to :order
end

class Food < ActiveRecord::Base
  has_many :food_orders
  has_many :orders, :through => :food_orders
  belongs_to :category
end

This is one of the versions of the form I have tried so far. But I am just baffled and do not know how to get fields for the FoodOrder Model.

<%= semantic_form_for [@restaurant, @order] do |f| %>
  <%= f.inputs do %>
    <%= f.input :comment %><br />
    <%= f.input :table_id %><br />
    <%# <%= f.input :foods, :as => :check_boxes %>
    <%= f.inputs :for => :foods do |food| %>
      <%= food %>
      <%= food.inputs :quantity %>
    <% end %>
  <% end %>
  <%= f.buttons do %>
    <%= f.commit_button %>
  <% end %>
<% end %>

My models have these attributes

create_table "food_orders", :force => true do |t|
  t.integer  "quantity",   :null => false
  t.decimal  "price",      :null => false
  t.integer  "food_id",    :null => false
  t.integer  "order_id",   :null => false
  t.text     "comment"
  t.datetime "created_at", :null => false
  t.datetime "updated_at", :null => false
end

create_table "foods", :force => true do |t|
  t.integer  "category_id",                     :null => false
  t.string   "name",                            :null => false
  t.string   "description"
  t.string   "image"
  t.decimal  "default_price",                   :null => false
  t.boolean  "active",        :default => true, :null => false
  t.datetime "created_at",                      :null => false
  t.datetime "updated_at",                      :null => false
end

create_table "orders", :force => true do |t|
  t.integer  "restaurant_id",                    :null => false
  t.integer  "user_id",                          :null => false
  t.integer  "table_id",                         :null => false
  t.decimal  "total",                            :null => false
  t.datetime "finished_at"
  t.datetime "created_at",                       :null => false
  t.datetime "updated_at",                       :null => false
end

Solution

  • Basically what I would end up doing is creating a FoodOrder object for each Food then only save the ones that have a value above 0.

    You could do that in a reject_if for the accepts_nested_attributes_for but that wouldn't deleted the old FoodOrders that someone might change from 3 to 0. So instead you will have to override the quantity= method for FoodOrder.

    I've never used formtastic before but I'm going to take a guess and say that this should work.

    <%= semantic_form_for [@restaurant, @order] do |f| %>
      <%= f.inputs do %>
        <%= f.input :comment %><br />
        <%= f.input :table_id %><br />
        <% @foods.each do |food|
          <%= f.semantic_fields_for :food_orders, 
                                @order.food_orders.detect_or_build_by_food(food)
                                do |food_order| %>
            <%= food %>
            <%= food_order.inputs :quantity %>
          <% end %>
        <% end %>
      <% end %>
      <%= f.buttons do %>
        <%= f.commit_button %>
      <% end %>
    <% end %>
    

    order.rb:

    class Order < ActiveRecord::Base
      belongs_to :user
      belongs_to :restaurant
      has_many :foods, :through => :food_orders
      has_many :food_orders do
        def detect_or_build_by_food(food)
          record = self.detect{ |food_order| food_order.food_id == food.id }
          if record.nil?
            record = self.build(:food => food)
          end
          record
        end
      end
    
      accepts_nested_attributes_for :food_orders
    end
    

    food_order.rb:

    class FoodOrder < ActiveRecord::Base
      belongs_to :food
      belongs_to :order
    
      def quantity=(quantity)
        if quantity <= 0
          self.destroy
        else
          self[:quantity] = quantity
        end
      end
    end
    

    I would hope that works but it is untested code, so yeah... Good Luck!