HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyrailsMinor

Refactoring contest: Rails accepts_nested_attributes_for a many-to-many associated model

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
refactoringaccepts_nested_attributes_forrailscontestassociatedmanymodel

Problem

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  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:

     

  Ricetta
  
  
  
         

         

  
      
  


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

```

Ingredienti

'dum

Solution

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

quantities_fields.html.erb:


   Ingredienti
    
     'dummy_hidden_fields' %>

    
    
        

        ...

        
          

        
        
         'btn btn-mini' %>       
       
     
 




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)
  })

Code Snippets

<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()" %>
%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)
  })

Context

StackExchange Code Review Q#10724, answer score: 3

Revisions (0)

No revisions yet.