1

I'm trying to let a user create Exercises with Equipment and Muscles in many-to-many relationships through their respective join tables( exercise_equipment, exercise_muscles ). I've gotten the form working for adding one equipment/muscle per exercise, but cannot figure out how to add a link to add another field to the form on the fly.

I've checked out RailsCasts, this post, have asked it as a side question on a previous post of my own, but simply cannot get this functionality to work. I'm fairly new to Rails 4 and am still trying to learn Javascript, but I'd love a thorough explanation of how to set this up the Rails 4 way!

My Models:

# id        :integer
# name      :string
# is_public :boolean
Exercise
    has_many :exercise_equipment
    has_many :equipment, :through => :exercise_equipment
    accepts_nested_attributes_for :exercise_equipment


# id           :integer
# exercise_id  :integer
# equipment_id :integer
# optional     :boolean
ExerciseEquipment
    belongs_to :exercise
    belongs_to :equipment
    accepts_nested_attributes_for :equipment


# id   :integer
# name :string
Equipment
    has_many :exercise_equipment
    has_many :exercises, :through => :exercise_equipment

My Controller Methods:

def new
  @exercise = Exercise.new
  @exercise.exercise_equipment.build
  @exercise.exercise_muscles.build
end

def create
  exercise = current_user.exercises.new( exercise_params )
  if exercise.save!
    redirect_to exercise
  else
    render 'new'
  end
end

views/exercises/new.html.erb

<h1>Create New Exercise</h1>

<%= form_for @exercise do |f| %>
  <%= render 'form', f: f %>
  <%= f.submit "New Exercise" %>
<% end %>

views/exercises/_form.html.erb

<%= f.label :name %><br />
<%= f.text_field :name, autofocus: true %>

<%= f.check_box :is_public %> Public

<%= f.fields_for :exercise_muscles do |emf| %>
  <%= emf.collection_select :muscle_id, Muscle.all, :id, :name, { include_hidden: false } %>
<% end %>

<%= f.fields_for :exercise_equipment do |eef| %>
  <%= eef.collection_select :equipment_id, Equipment.all, :id, :name, { include_hidden: false } %>
  <%= eef.check_box :optional %> Optional
<% end %>
Community
  • 1
  • 1
s_dolan
  • 1,196
  • 1
  • 9
  • 21

2 Answers2

2

Ajax

Cannot figure out how to add a link to add another field to the form on the fly

The "Rails way" of doing that is to "pull" a new instance of the fields_for block from an ajax request.

The reason why Ajax is recommended is because it's the "Rails way" to do it - completely modular & extensible:

#config/routes.rb
resources :exercises do
   get :add_field, on: :collection
end

#app/models/exercise.rb
Class Exercise < ActiveRecord::Base
   ...
   def self.build #-> allows you to call a single method
      exercise = self.new
      exercise.exercise_equipment.build
      exercise.exercise_muscles.build
      return
   end
end

#app/controllers/exercises_controller.rb
Class ExercisesController < ApplicationController
   def add_field
      @exercise = Exercise.build
      respond_to do |format|
         format.html
         format.js
      end
   end
end

#app/views/exercises/new.html.erb
<%= form_for @exercise do |f| %>
   <%= render "fields", locals: { f: f } %>
   <%= f.submit %>
<% end %>

#app/views/exercises/add_field.js.erb
$("#form_element").append("<%=j render "exercises/form", locals: { exercise: @exercise } %>");

#app/views/exercises/_form.html.erb
<%= form_for exercise do |f| %>
   <%= render "fields", locals: { f: f } %>
<% end %>

#app/views/exercises/_fields.html.erb
<%= f.fields_for :exercise_muscles, child_index: Time.now.to_i do |emf| %>
    <%= emf.collection_select :muscle_id, Muscle.all, :id, :name, { include_hidden: false } %>
<% end %>
<%= link_to "New Field", exercises_add_fields_path, remote: :true %>

<%= f.fields_for :exercise_equipment, child_index: Time.now.to_i do |eef| %>
   <%= eef.collection_select :equipment_id, Equipment.all, :id, :name, { include_hidden: false } %>
   <%= eef.check_box :optional %> Optional
<% end %>
<%= link_to "New Field", exercises_add_fields_path, remote: :true %>

This will give you the ability to create the fields through an ajax call; which is the correct way to do it

Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • I guess what I don't understand is where "#form_element" is coming from. Do I need to add it as a class to my fields_for? – s_dolan Aug 15 '14 at 12:36
  • Also, won't these link_to's add one of each form, instead of either adding a new muscle or equipment field? A valid use case would be an exercise workout out 5 muscles but not using any equipment. – s_dolan Aug 15 '14 at 12:42
  • Okay, yes you're correct. I was going to add the ability for the partial to differentiate between the two - would you like me to update? – Richard Peck Aug 15 '14 at 13:08
  • I think I see how you'd do it by just making two different paths, but if you could update your answer for me to check against that would be great. And maybe add where the #form_element is being declared at? – s_dolan Aug 15 '14 at 13:10
  • You'll have to declare that in your original view - it will be the `
    ` you wish to append the fields to
    – Richard Peck Aug 15 '14 at 13:17
  • Ah, I see. I'll check out your updates later tonight and accept the answer if I can get it working. Thanks so much for your help, Rich! – s_dolan Aug 15 '14 at 13:21
1

After attempting to use Rich's solution, I wanted to find one that was a bit more minimal in terms of the code used. I found the gem Cocoon, which works great and was very simple to integrate.

My main _form view:

<div class="form-group">
  <%= f.label :name %><br />
  <%= f.text_field :name, autofocus: true %>
</div>

<div class="form-group">
  <%= f.check_box :is_public %> Public
</div>

<div class="form-group">
  <%= f.fields_for :exercise_muscles do |emf| %>
    <%= render 'exercise_muscle_fields', :f => emf %>
  <% end %>
  <%= link_to_add_association 'Add Muscle', f, :exercise_muscles %>
</div>

<div class="form-group">
  <%= f.fields_for :exercise_equipment do |eef| %>
    <%= render 'exercise_equipment_fields', :f => eef %>
  <% end %>
  <%= link_to_add_association 'Add Equipment', f, :exercise_equipment %>
</div>

As can be seen here, the addition of a simple "link_to_add_association" method takes care of all of the Javascript in the background. For future readers, here are the partials that each of these form-groups contain:

_exercise_muscle_fields:

<%= f.collection_select :muscle_id, Muscle.all.order( 'muscle_group_id ASC' ), :id, :name, { include_hidden: false } %>

_exercise_equipment_fields:

<%= f.collection_select :equipment_id, Equipment.all.order( 'name ASC' ), :id, :name, { include_hidden: false } %>
<%= f.check_box :optional %> Optional
s_dolan
  • 1,196
  • 1
  • 9
  • 21