patternrubyrailsMinor
Refactoring contest: Rails accepts_nested_attributes_for a many-to-many associated model
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:
Which, of course, has many ingredients:
Through quantities:
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.
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:
Where the quantities_fields partial is the following (now it comes the mayhem):
```
Ingredienti
'dum
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
endWhich, of course, has many ingredients:
class Ingredient < ActiveRecord::Base
has_many :quantities, dependent: :destroy
has_many :recipes, through: :quantities
attr_accessible :name, :availability, :unit
endThrough 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
endPlease 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).qtyBy 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:
Instead of using a hidden
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.