In this post I'm going to walk through a skeleton app with basic functionality to create nested forms, which enable the creation of new objects in multiple models at the same time. For the example application, we'll create a workout tracker with a form that can create workouts and the specific exercises within that workout.
Start by creating a new application and cding into it.
rails new strength
cd strength
As we're just creating an example application, let's use rails scaffolds to get up and running with our two models relatively quickly. The two scaffolds in this example are called Workout and Exercise.
rails g scaffold Workout date:datetime name:string energy:integer length:integer time_of_day:string
rails g scaffold Exercise name:string sets:integer reps:integer weight:integer
rake db:migrate
Database Configuration
Set up the models with a has_many relationship. Each workout has_many exercises and each exercise belongs_to a workout
### workout.rb
class Workout < ActiveRecord::Base
has_many :exercises
end
### exercise.rb
class Exercise < ActiveRecord::Base
belongs_to :workout
end
At this point it should become apparent that we will also need the workout_id in the exercise database. This will allow us, for example, to see that the Squats exercise belongs specifically to the Legs workout. Luckily, this is quickly fixed up with a special index migration command.
rails g migration add_workout_id_to_exercise workout_id:index
rake db:migrate
The migration file should look something like this.
Next we'll add some seeds to both workouts and exercises to get our database a little more flushed out and easier to work with as we transition to manipulating the browser-based views. In seeds.rb:
rake db:seed
At this point if we head into the rails console, we should be able to see confirmation that not only have our objects been created, but each of the exercises are matched with a corresponding workout_id.
Nested Form Functionality
Now let's transition to actually making it so that we can add and modify exercises from workout's new and edit actions. Let's work back to front, from the model to the view. First, workout.rb will have to accept nested attributes from exercises:
class Workout < ActiveRecord::Base
has_many :exercises
accepts_nested_attributes_for :exercises
end
Next up is the controller, where we'll want to make it easy to create new exercises in the new and edit models. In workouts_controller.rb:
# GET /workouts/new
def new
@workout = Workout.new
@workout.exercises.build
end
# GET /workouts/1/edit
def edit
@workout.exercises.build
end
Something you might notice at this point is that the exercises_attributes have not been white-listed as permitted workout parameters. We can rectify that quickly with the following:
### workouts_controller.rb
private
# edited for brevity
def workout_params
params.require(:workout).permit(:date, :name, :energy, :length, :time_of_day, exercises_attributes: [ :id, :name, :sets, :reps, :weight])
end
Finally, let's touch on the views. Starting with workout#show, I added in a simple loop at the bottom of the html that adds in each of the exercises that correspond to that workout id. It looks like this:
<!-- In app/views/workouts/show.html.erb --> <h4>Exercises</h2> <% if @workout.exercises.any? %> <ul><% @workout.exercises.each do |exercise| %> <li><strong><%= link_to exercise.name, exercise %></strong></li> <li>Sets: <%= exercise.sets %></li> <li>Reps: <%= exercise.reps %></li> <li>Weight: <%= exercise.weight %></li> <% end %> <% else %> <p><strong>Please add some exercises to this workout</strong></p> <% end %>
But it would be silly to just have the ability to see exercises within workouts and not create or edit them, so let's also add exercise functionality to app/views/workouts/_form.html.erb
<!-- EXERCISES --> <h4>Exercises</h4> <%= f.fields_for :exercises do |exercise| %> <div class="field"> <%= exercise.label :name %><br> <%= exercise.text_field :name %> </div> <div class="field"> <%= exercise.label :sets %><br> <%= exercise.number_field :sets %> </div> <div class="field"> <%= exercise.label :reps %><br> <%= exercise.number_field :reps %> </div> <div class="field"> <%= exercise.label :weight %><br> <%= exercise.number_field :weight %> </div> <% end %>
Note here that this block utilizer fields_for, which is just a helper method. At this point you won't trick anyone into thinking you've made the most beautiful form in the world, but you should have the basic nested form functionality promised at the outset.
If you're interested, here's the source code on Github. Try it out, tweak it a little bit to suit your needs, and let me know if you have any questions in the comments!