Building a Form with Ruby On Rails Using Multiple Instances of A Model (Reloaded)
In 2018, I published this article, Building A Quiz with Ruby on Rails Using Multiple Instances of A Model, showing how I created a 120-question quiz with Ruby on Rails using multiple instances of a model. It worked great for that use case but when I tried to use a modified version of the code to create an interview form with multiple text fields, I couldn’t get it to work!
In this article, I will show you how I created a form containing interview questions as text fields for this specific use case. The 4 day ordeal of trying to get this to work offered me the opportunity to better understand Ruby and Ruby on Rails.
USE CASE: For my current project, they needed a way to publish interviews from their target audience and needed a form to collect personal information and the answers for each question. This form would need to be able to submit the personal information into its own table and the answers for each question into another table. Each answer would need to be associated with the personal information record and a question.
The needed structure:
- Story — Personal Information
- Story Question — Interview Question
- Story Answer — Answer for each question
The database structure (stories, story_questions, story_answers):
This will differ depending on what you’re working on but I’ve included it so you can have a full understanding of what the new feature needs to do.
create_table "stories", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.boolean "published"
t.boolean "agree"
t.string "first_name"
t.string "last_name"
t.string "slug"
t.string "email"
t.string "website"
t.string "twitter"
t.string "instagram"
t.text "bio"
t.string "photo"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
endcreate_table "story_questions", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.integer "sort"
t.boolean "active"
t.string "question"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
endcreate_table "story_answers", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
t.text "answer"
t.integer "story_question_id"
t.integer "story_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["story_id"], name: "index_story_answers_on_story_id"
t.index ["story_question_id"], name: "index_story_answers_on_story_question_id"
end
The models (story.rb, story_question.rb, story_answer.rb):
The models included the required associations for each part of the feature. I’m not 100% set on having the question remove answers if the question is removed but for now, it’s ok.
class Story < ApplicationRecord has_many :story_answers, dependent: :destroy, :inverse_of => :story
accepts_nested_attributes_for :story_answers, :allow_destroy => trueendclass StoryQuestion < ApplicationRecord has_many :story_answers, dependent: :destroy, :inverse_of => :story_question
accepts_nested_attributes_for :story_answers, :allow_destroy => true
include RailsSortable::Model
set_sortable :sort, without_updating_timestamps: trueendclass StoryAnswer < ApplicationRecord belongs_to :story, :inverse_of => :story_answers
accepts_nested_attributes_for :story belongs_to :story_question, :inverse_of => :story_answers
accepts_nested_attributes_for :story_questionend
The needed routes:
resources :stories, only: [:index, :show, :new, :create] do
resources :story_answers, only: [:new, :create]
end
get ‘nature-story’ => ‘stories#new’
post ‘nature-story’ => ‘stories#create’
The stories_controller.rb:
This controller and the form is where the magic happens. I have to reluctantly admit it took way longer than it should for me to figure out how to get this feature to work. Creating multiple text fields for the same model wasn’t hard to figure out. Showing each question for each empty answer and being able to pass the question id to the answer took a little while. Once I figured it out, it was embarrassingly easy.
Under the “new” action, we are creating the new story with Story.new. The “new” page also lists all the questions in an “info” section so interviewees can review them before submitting the form so we make @story_questions available to the view for this purpose and for use in the builder block below it in the controller. The real magic happens when for each of the questions we build the story_answer fields using the rails builder. We use @story_questions.each do |q| @story.story_answers.build end.
Finally we need to add the story_answers_attributes parameters to the permitted parameters at the end of the controller.
class StoriesController < ApplicationController
before_action :set_story, only: %i[ show ]def index
...
enddef show
...
enddef new
@story = Story.new
@story_questions = StoryQuestion.order(:sort).all
@story_questions.each do |q|
@story.story_answers.build
end
enddef create
@story = Story.new(story_params)
@story.published = 0
@story.saverespond_to do |format|
if @story.save
StoryMailer.new_story(@story).deliver_later
format.html { redirect_to new_story_path(anchor: 'interview'), notice: "Your nature story interview has been sent." }
format.json { render :show, status: :created, location: @story }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @story.errors, status: :unprocessable_entity }
end
end
endprivatedef set_story
@story = Story.find_by_slug!(params[:id])
enddef story_params
params.require(:story).permit(:published, :agree, :first_name, :last_name, :slug, :email, :website, :twitter, :instagram, :bio, :photo, story_answers_attributes: [:id, :answer, :story_question_id, :story_id])
endend
The _form.html.rb for a New Story:
The next critical part of this feature was getting the form right. You may notice from my last article, I was using the form_tag and fields_for. That wasn’t appropriate or needed for this use case. I only needed the simple_fields_for tag (I’m using the simple_form gem).
I’m using :story_answers in the simple_fields_for code instead of @story_answers because I’m using the builder for these fields. To display the questions, I set a local variable “q” to a story question where the :sort field of the question was equal to the index of the story_answer. I did the + 1 so the index starts at 1 and not 0. Until this situation, I wasn’t completely familiar with using indexes or that the FormBuilder object automatically gives you access to the index so you can do something like <%= r.index %> and the index numbers would be listed.
Once I had access to the question, I could use q to list the sort number and question about the answer text field and then pass the question id as q.id to the hidden field so the answer would be associated with the correct question upon save.
<%= simple_form_for @story, :url => nature_story_path do |f| %>...
<div class="col-12 learts-mb-50">
<span style="color:red;">Please try to answer all questions thoughtfully.</span>
<hr>
<br>
<%= f.simple_fields_for :story_answers do |r| %>
<% q = StoryQuestion.where(sort: r.index + 1).first %>
<strong><%= q.sort %>. <%= q.question %></strong>
<%= r.input :answer, label: false %>
<%= r.input :story_question_id, :as => "hidden", :input_html => { :value => q.id }, label: false %>
<% end %><br>
</div>
...<div class="col-12 text-center learts-mb-50">
<button type="submit" class="btn btn-dark btn-outline-hover-dark">Submit Interview</button>
</div>
</div></div><% end %>
The interview form page:
This is what the final page looks like. The fields above the questions are for the “personal information” that creates the “story” and the questions will be submitted into their own table as well.
There are 14 questions total so the form is shortened in the image just to give you an idea of what’s happening.
That’s it! This is a little less complicated than my other solution and works well for this kind of use case. The other solution works well for more complex forms with multiple choice questions and radio fields.
Rails version: 6.0.3