Dynamically add and remove input field in rails without javascript

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

9 thoughts on “Dynamically add and remove input field in rails without javascript

  1. 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!!

  2. Thanx! Good start for beginners who think all that “javascripty” thing is so overwhelming !

  3. 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.

  4. 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

  5. I lost my nerve with RoR, but this helped me a lot . Thank you!

  6. 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.

Leave a comment