Building complex forms with Rails11th October 2018
Building a form using Ruby on Rails sometimes can get a little bit more complicated than you think, when you want to use more than one model on it, even more, if your expertise on building forms is not extensive, and that's the moment when you wonder: Is there a better way to do this?
In Ruby on Rails the simplest way to do it is using "nested attributes" and creating a "nested form", but... What are these?
A nested attribute, allows you to create and save attributes from an associated model through its parent, and a nested form, is a form with another form inside of it. The second form is going to have the nested attributes you want to save.
Having said that, let's create an example using nested attributes and a nested form.
Imagine you want to create a system for a company that wants to evaluate its employees using a feedback questionnaire. To create this example, we are going to have 3 models: Feedback, Question and Answer.
Now, let's define the attributes that our models need:
- Feedback won't have extra attributes.
- Question will have a question_title attribute, which is going to be the question added by a user from the company.
- Answer will have an answer attribute that is going to keep the reply from the employee, by using a feedback_id to know who is the owner of this answer and a question_id, to know what is the question for this answer.
Having explained the requirements that we need, create the project with the mentioned models and their attributes, and finally run migrations.
Once we have everything in place, We need to create the relations between models:
- Feedback will have many answers, so we'll have to add in the model:
- Question will also have many answers, as we did with the Feedback model, we need to add:
- Finally, an Answer will belong to a feedback and a question, what we have to do here, is write on the Answer model:
Now that we have done this, let's test the relations between models in our console, but first of all, we need to create a Feedback object:
Secondly, let's create a question object
Question.create!(question_title: 'first question')
And finally, we need to create an answer that is going to have the ID from the previous feedback and question created.
Answer.create!(answer: 'first answer', feedback_id: Feedback.last.id, question_id: Question.last.id)
Right away let's test the relation between the answer and the feedback, we should get the answer that we created.
Now, test if the question we created is related to the previous answer, we should get the question we created.
Doing this give us the certainty that our relations work. Now let's proceed with the form creation.
In our Feedback form that will be placed on feedbacks/new.html.erb, there will be an answer field for each existing question. Right now, we have just 1 question created, add 2 more questions to have more answer fields in our form.
Before the form creation, in our FeedbacksController we need to create the "new" method like this:
def new @feedback = Feedback.new Question.all.each do |question| @feedback.answers.build(question_id: question.id) end end
On the "new" method, we are creating an instance of a Feedback object, also for each existing question we are building an Answer object that is going to contain the id from the question it will belong to.
Let's continue with the form creation, to be able to display the answer objects on the form, we need to indicate in the Feedback model to accept nested attributes from the model Answer, we can do it by writing:
To visualize it, we need to create a form for @feedback and then inside this form we need to create another form for the nested attributes, this is known as a "nested form".
First of all create the @feedback form and then continue adding the fields_form method like this:
<%= form_for(@feedback) do |form| %> <%= form.fields_for :answers do |answer_form| %> <div> <%= answer_form.text_area :answer %> </div> <% end %> <div> <%= form.submit %> </div> <% end %>
Now that we can see our answer fields, we should have 3 answer fields displayed because there are 3 questions in our question table. But, where are the questions on the form? Remember when we built the Answer objects in the "new" method on FeedbacksController and when we added the id from each question to build an answer object?
Thanks to this, each answer object in our form contains a question_id which will help us find the related question.
Let's get the question using the provided method "object" from the answer_form like this:
<%= answer_form.object.question.question_title %>
By doing this we are telling Rails to display which is the question related to each object answer.
Another thing we can't miss is that we need to send on the form the id for the related question because there is a relation between the answer and the question. If we don't add it, we will get an error saying that the question id is not included and should be included when we try to save the feedback object. We can add it on a hidden_field so the user won't be able to see it on the form:
<%= answer_form.hidden_field :question_id, value: answer_form.object.question_id %>
Finally, our form should look as follows:
<%= form_for(@feedback) do |form| %> <%= form.fields_for :answers do |answer_form| %> <div> <p><%= answer_form.object.question.question_title %></p> <%= answer_form.text_area :answer %> <%= answer_form.hidden_field :question_id, value: answer_form.object.question_id %> </div> <% end %> <div> <%= form.submit %> </div> <% end %>
At this point our form is complete, we now only need to add the method create on FeedbacksController, that will create and save our feedback with the answers:
def create @feedback = Feedback.new(params.require(:feedback).permit(answers_attributes: [:answer, :question_id])) if @feedback.save redirect_to feedbacks_path, notice: 'The feedback was successfully created.' else redirect_to new_feedback_path end end
We are done at this point, now run
rails server open your browser, navigate to the /new path and create a feedback.
To test what we just did, open your console and look for the last Feedback created
And then look for its answers:
We should get the related answers from this feedback. If we want to know which is the question related to each answer, we can write as we previously did:
And the related question should be displayed.
With everything that we did, we have created the core of a feedback questionnaire application. Now, you can add more functions, like the ability to select a user because you might want to evaluate it using this questionnaire, then a Feedback is going to belong to a User and a User may have many feedbacks, but it is up to you… Have fun and improve it!