8
\$\begingroup\$

I found myself struggling when it came to refactor the following (working) code. Since can't be found a lot of documentation about Rails 'accepts_nested_attributes_for' applied to a many-to-many relationship (furthermore, with ancillary data), I figured out it would be interesting to challenge this brave community about it! :-)

The app logic is quite simple, we have a recipe model:

class Recipe < ActiveRecord::Base  
  has_many :quantities, dependent: :destroy
  has_many :ingredients, through: :quantities   
  accepts_nested_attributes_for :quantities, :reject_if => lambda { |dq| dq[:ingredient_id].blank? || dq[:qty].blank? }, :allow_destroy => true 

  attr_accessible :name, :description, :kind  
end          

Which, of course, has many ingredients:

class Ingredient < ActiveRecord::Base 
  has_many :quantities, dependent: :destroy
  has_many :recipes, through: :quantities

  attr_accessible :name, :availability, :unit
end            

Through quantities:

class Quantity < ActiveRecord::Base 

  belongs_to :recipe
  belongs_to :ingredient

  attr_accessible :recipe_id, :ingredient_id, :qty  

  # There can be only one of each ingredient per recipe
  validates_uniqueness_of :ingredient_id, scope: :recipe_id 
  validates_numericality_of :qty, greater_than: 0 

end

Please note: every ingredient has an 'in stock' availability (and a unit of measurement for it), while every recipe needs a given 'quantity.qty*' of some ingredients in order to be prepared.

* Quantity.where(ingredient_id: ingredient.id, recipe_id: recipe.id).qty

By the way, do you know a more concise way to get some ingredient quantity.qty for some recipe?

Back to the main context, it is mainly about the following views. The difficult part regards the fact that the fields are dynamically displayed or removed through jQuery, as in this Railscast: http://railscasts.com/episodes/197-nested-model-form-part-2.

So the next one displays the form to create and edit a recipe:

<%= simple_form_for(@recipe) do |f| %>     
<%= render 'shared/error_messages', object: @recipe %>
<fieldset>
  <legend>Ricetta</legend>
  <%= f.input :name %>
  <%= f.input :description %>
  <%= f.input :kind, collection: Recipe::KINDS, prompt: "Scegli..." %>
  <%= f.input :cooked %>       

  <%= render 'quantities_fields' %>       

  <div class="form-actions">
      <%= f.button :submit, "Confirm" %>
  </div>
</fieldset>
<% end %> 

Where the quantities_fields partial is the following (now it comes the mayhem):

<fieldset class="nested">
    <legend>Ingredienti</legend>
    <ol id='ingredients'> 
        <%= render :partial => 'dummy_hidden_fields' %>

        <% for dq in @recipe.quantities %>  
        <li class="fields">
            <%= select_tag 'recipe[quantities_attributes][][ingredient_id]', 
                options_from_collection_for_select(ingredients_for_select, 'id', 'name_unit', dq.ingredient_id) -%>  
            <%= text_field_tag 'recipe[quantities_attributes][][qty]', ingredient_qty_for_select(dq), size: 3, placeholder: "Q.tà" %> 
            <!-- the id must be POST in order to trigger update instead of create when appropriate! -->
            <%= hidden_field_tag 'recipe[quantities_attributes][][id]', dq.id %>  
            <!-- the following must be changed to '1' via js in order to destroy this quantity -->  
            <%= hidden_field_tag 'recipe[quantities_attributes][][_destroy]', 0  %>
            <%= link_to_function "elimina", "remove_ingredient(this)", :class => 'btn btn-mini' %>       
        </li>   
        <% end %> 
    </ol> 
    <%= link_to_function "Add ingredient", "add_ingredient()" %>
</fieldset>    

Yes, I wasn't able to use 'fields_for' because I couldn't figure out a proper way.

The 'dummy_hidden_field" partial is used to create some initially hidden fields to be displayed through jQuery if a new ingredient is needed:

<li class="fields hidden">
    <%= select_tag 'recipe[quantities_attributes][][ingredient_id]', options_from_collection_for_select(ingredients_for_select, 'id', 'name_unit') -%>  
    <%= text_field_tag 'recipe[quantities_attributes][][qty]', nil, size: 3, placeholder: "Q.tà" %> 
    <%= hidden_field_tag 'recipe[quantities_attributes][][_destroy]', 0  %>
    <%= link_to_function "elimina", "remove_ingredient(this)", :class => 'btn btn-mini' %>  
</li>

And for last the above-mentioned jQuery functions:

function remove_ingredient(link) {
  $(link).prev("input[type=hidden]").val("1");
  $(link).closest(".fields").hide();
}

function add_ingredient() {
    var ol = $('ol#ingredients');
    ol.find('li:first').clone(true).appendTo(ol).removeClass('hidden');
}   

For any further information please don't hesitate to ask!

\$\endgroup\$
3
\$\begingroup\$

You can try this, taken from Ruby on Rails API:

quantities_fields.html.erb:

<fieldset class="nested">
   <legend>Ingredienti</legend>
   <ol id='ingredients'> 
    <%= render :partial => 'dummy_hidden_fields' %>

    <%= fields_for :quantities do |recipe_quantities| %>
    <li class="fields">
        <%= recipe_quantities.select :ingredient_id, options_from_collection_for_select(ingredients_for_select, :id, :name_unit, recipe_quantities.object.ingredient_id) %>

        ...

        <%#  fields_for will automatically generate a hidden field to store the ID of the record, so the following line is not necessary %>
        <%#= recipe_quantities.hidden_field_tag 'recipe[quantities_attributes][][id]', dq.id %>  

        <%# when you use a form element with the _destroy parameter, with a value that evaluates to true, you will destroy the associated model (eg. 1, ‘1’, true, or ‘true’) %>
        <%= recipe_quantities.hidden_field :_destroy, value: 0 %>
        <%= link_to_function "elimina", "remove_ingredient(this)", :class => 'btn btn-mini' %>       
    </li>   
    <% end %> 
</ol> 
<%= link_to_function "Add ingredient", "add_ingredient()" %>

Instead of using a hidden li to add new ingredients, to use a template, like in this example (written in haml):

%h1 Edit Task Collection
= form_tag task_collection_update_path, :method => :put do
  %table#tasks
    %tr
      %th Name
      %th Priority
        - @tasks.each do |task|
      = fields_for 'tasks[]', task, :hidden_field_id => true do |task_form|
        = render :partial => 'task_form', :locals => {:task_form => task_form}

  = button_tag 'Save'

= button_tag "Add task", :type => :button, :id => :add_task

%script{:type => 'html/template', :id => 'task_form_template'}
  = fields_for 'tasks[]', Task.new, :index => 'NEW_RECORD', :hidden_field_id => true do |task_form| render(:partial => 'task_form', :locals => {:task_form => task_form}); end

:javascript
  $(function(){
    task_form = function(){ return $('#task_form_template').text().replace(/NEW_RECORD/g, new Date().getTime())}
    var add_task = function(){ $('#tasks').append(task_form()) }
    $('#add_task').on('click', add_task)
  })
\$\endgroup\$
  • 1
    \$\begingroup\$ +1 for putting the effort in - and not running away with all those downvotes! Keep answering, CR needs you! \$\endgroup\$ – Mathieu Guindon Feb 26 '14 at 0:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.