when dealing with nested form field using accepts_nested_attributes_for in ruby on rails, I always tempted to ada feature where the user can dynamically add or remove form field on the form. Usually people achieve this with the help of javascript function. But I want to use plain html for the sake of ‘progressive enhancement’, if that’s works than I can apply some javascript to make it more interactive, it turns out to be pretty simple with ruby on rails.
first, the models
# recipe model class Recipe < ActiveRecord::Base has_many :ingredients accepts_nested_attributes_for :ingredients, :allow_destroy => true validates_presence_of :name end # Ingredient model class Ingredient < ActiveRecord::Base belongs_to :recipe end
then the controller
class RecipesController < ApplicationController def index @recipes = Recipe.all end def show @recipe = Recipe.find(params[:id]) end def new @recipe = Recipe.new @recipe.ingredients.build # build ingredient attributes, nothing new here end def create @recipe = Recipe.new(params[:recipe]) if params[:add_ingredient] # add empty ingredient associated with @recipe @recipe.ingredients.build elsif params[:remove_ingredient] # nested model that have _destroy attribute = 1 automatically deleted by rails else # save goes like usual if @recipe.save flash[:notice] = "Successfully created recipe." redirect_to @recipe and return end end render :action => 'new' end def edit @recipe = Recipe.find(params[:id]) end def update @recipe = Recipe.find(params[:id]) if params[:add_ingredient] # rebuild the ingredient attributes that doesn't have an id unless params[:recipe][:ingredients_attributes].blank? for attribute in params[:recipe][:ingredients_attributes] @recipe.ingredients.build(attribute.last.except(:_destroy)) unless attribute.last.has_key?(:id) end end # add one more empty ingredient attribute @recipe.ingredients.build elsif params[:remove_ingredient] # collect all marked for delete ingredient ids removed_ingredients = params[:recipe][:ingredients_attributes].collect { |i, att| att[:id] if (att[:id] && att[:_destroy].to_i == 1) } # physically delete the ingredients from database Ingredient.delete(removed_ingredients) flash[:notice] = "Ingredients removed." for attribute in params[:recipe][:ingredients_attributes] # rebuild ingredients attributes that doesn't have an id and its _destroy attribute is not 1 @recipe.ingredients.build(attribute.last.except(:_destroy)) if (!attribute.last.has_key?(:id) && attribute.last[:_destroy].to_i == 0) end else # save goes like usual if @recipe.update_attributes(params[:recipe]) flash[:notice] = "Successfully updated recipe." redirect_to @recipe and return end end render :action => 'edit' end def destroy @recipe = Recipe.find(params[:id]) @recipe.destroy flash[:notice] = "Successfully destroyed recipe." redirect_to recipes_url end end
the views ( form partial )
<% form_for @recipe do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <p> <%= f.label :description %><br /> <%= f.text_area :description, :rows => 4, :cols => 50 %> </p> <h3>Ingredients</h3> <% f.fields_for :ingredients do |ing| %> <p> <%= ing.label :name %> <%= ing.text_field :name, :size => 50 %> <%= ing.check_box :_destroy %> <%= ing.label :_destroy, 'delete' %> </p> <% end -%> <p> <%= f.submit 'Add ingredient', :name => "add_ingredient" %> <%= f.submit 'Delete checked ingredients', :name => "remove_ingredient" %> <%= f.submit %> </p> <% end %>
and the code in actions…
now that the basic foundation is working, adding some javascript to add an remove nested fields should be fun :)
Thank you!!! I’ve been breaking my head over this for a while. The only way I was able to make it work is with JS without progressive enhancement.
Thanks again!!
Thanx! Good start for beginners who think all that “javascripty” thing is so overwhelming !
Can you post where the add ingredient method as when I tried it when I do add ingredient it tries to create the recipe and finishes.
It’s all in the update methods, the part with the params[:add_ingredient] part does the job
I have had cleaner results using, instead of a separate submit button, a link to a different action on my controller. For example:
in recipe controller
def add_ingredient
@recipe = Recipe.find(params[:id])
@recipe.ingredients.build
render :edit
end
then in the view
link_to “Add Ingredient”, controller: :recipes, action: :add_ingredient, id: @recipe
I lost my nerve with RoR, but this helped me a lot . Thank you!
do you have a project with this to topic?
i like this
I did this but when I click to add an additional field, it just tries to submit the whole form and does not add any additional fields.