Learn how to use accepts_nested_attributes_for and fields_for to create forms that include associated models in them
class ForumThreadsController < ApplicationController def new @forum_thread = ForumThread.new @forum_thread.forum_posts.new end def create @forum_thread = current_user.forum_threads.new forum_thread_params @forum_thread.forum_posts.first.user_id = current_user.id if @forum_thread.save redirect_to @forum_thread else render action: :new end end private def forum_thread_params params.require(:forum_thread).permit(:subject, forum_posts_attributes: [:body]) end end
class ForumThread < ActiveRecord::Base belongs_to :user has_many :forum_posts accepts_nested_attributes_for :forum_posts validates :subject, presence: true validates_associated :forum_posts end
Continuing on our forum series in GoRails, we're going to talk about using the nested attributes in your forums so that you can create multiple records at the same time, and they'll automatically be associated. That's what we're going to do today, and the way we're going to do that is we're going to create a forum thread that also has a forum post in the form, and when you submit that, it will create both records, and you'll be able to create the subject for the thread and the body post all at once.
The first thing you need to do, of course, is set up your forum thread in your controller, so you want to create a new one, but you also want to create a forum post for this thread. We'll create both of those in memory and have that one associated with it and this is used so that the fields for in your new action can actually render the forum post's attributes. So let's dive into our forum now.
Inside our form for the forum thread, we can see that we have just the standard form_for, we also have the regular text_field and the submit button, and that’s about it. If we open this in our browser we can see that you get the subject field and the create button, and that’s all there is to it. Now if we want to actually create the fields for the nested attributes, we can do f.fields_for, and we pass in a symbol in this case for the object that we want, so we want fields_for the forum posts, and this is going to be given a block, and the reason why you want to save this variable p is so that it’s different than the parent. The way that this works is that the p variable knows it’s a forum thread, and it knows that the child is going to be of a forum_post type. That is going to create the proper names in our text fields so long as we do something like p.text_field. So if you use p.text_field it will actually use the forum thread and the forum post in the name of the field and submit it across appropriately to Rails. We want to create a
<%= form_for @forum_thread do |f| %>
<%= f.fields_for :forum_posts do |p| %> <div class="form-group"> <%= p.text_area :body, placeholder: "Add a comment", rows: 10, class: "form-control" %> </div> <% end %> <div class="form-group"> <%= f.submit %> </div>
<% end %>
This is actually going to render now because of the forum posts that we created in memory, and it will display the text area’s body for us, so now if we refresh this page, we’ll see the body and the subject, and we’ll be able to submit these once we update the controllers create action to handle this appropriately, so let’s dive into that now.
There are a handful of things that we need to do in our controller and model to actually make that form submit data correctly to Rails. So the first thing of course, you need to create the thread, and we’re going to use the current user’s forum threads association to make that new thread, and the reason we’ll do that is because this will automatically assign the user id on that new thread. And as you would expect with strong params, we want to make a forum_thread_params method, and it’s going to have:
@forum_thread = current_user.forum_threads.new forum_thread_params
params.require(:forum_thread).permit(:subject, forum_posts_attributes: [:body])
We use forum_posts_attributes: [:body] to also permit the nested forum posts, and we do that by pointing to an array of attributes on that object, so we want to accept the body on a forum post when it gets submitted inside of a forum thread. Now this works to assign it, however we actually need to tell the model that it can allow forum posts attributes, so if we go into our forum_thread.rb (model), we can add an accepts_nested_attributes for :forum_posts, you pass in the symbol for the same name as your association, so in this case: forum posts. And the other thing that we want to make sure happens is that validations will run on forum threads when we save it but we also want validations to run on all of the nested forum posts as well, so you can pass in a validates_associated (method) and you give it the same :forum_posts association name. That will make sure that the assignment of attributes works, to create them on nested models, as well as running the validations on the nested models. So if we go back to the forum threads controller, we can do the typical:
#stuff from earlier
render action: :new
One last thing before we move on from here is that we want to make sure that these forum threads are assigned so we made sure that they’re assigned to the user_id, but we want to also make sure the forum posts underneath it are assigned their user_id as well.
We have that first forum post, and that one is created because of (@forum_thread.forum_posts.new) and we should only have one. And the form will take all of those that we created here and display them. So we only have one, and that means that we can set the user_id = current_user.id, and that should automatically set that. We don’t ever want that to be passed in as a parameter in the field or in the form, because then you could impersonate someone else if you edit the html, so we want to make sure that these two lines hard code that user id and that you could never change that from editing the HTML in the field
@forum_thread.forum_posts.first.user_id = current_user.id
Next episode we’re going to talk about using the div_for method to highlight and automatically scroll people down to a section of the page, so as you can see here, when we loaded this, it automatically highlighted this post as the new post on this page and then scrolled you down automatically. So we’ll talk about that and how it’s useful next episode, and I will see you next week.
Transcript written by Miguel
What markdown parser gem did you use for this episode Chris?
I'm using pygments.rb inside of Github's html-pipeline with a handful of other gems to improve the Markdown parsing:
gem "html-pipeline", "~> 1.9.0"
gem "rinku", "~> 1.7.3"
gem 'github-linguist', '~> 3.1.2'
gem "github-markdown", "~> 0.6.4"
gem 'pygments.rb', '~> 0.6.0'
You flew by the form pretty fast. Can you include that code on this page as well. I don't have the view files, so create them manually? Thanks!
I'm very new at this, so I have a few questions.
How come my forum_threads_controller was blank, but the video started with some code already?
Also, my views folder has none of the same files that the video has at this point? Are will we be creating these as we go?
I skipped a few of those pieces to focus on the topic in this episode. The controller and views are basically just those you get from generating a scaffold.
In the future, I'll be sure to include the code for you in between.
I have tried this but seem to have a problem.
When I do:
@post = Post.new
I get a undefined method 'build' for nil:class
but it will work if I do this:
@post = Post.new
has_one: :content, dependent: :destroy
Any ideas why?
has_one and has_many are different and you'll interact with them differently.
has_one :content makes methods like @post.build_content and @post.create_content
has_many :contents makes methods like @post.contents.build and @post.contents.create
Much more information here: http://api.rubyonrails.org/...
thanks for the reply Chris Oliver!
Most of the examples I saw were using has_many relationships so I guessed I missed it.
There are examples tho that I've seen in strong params:
params.require(:forum_thread).permit(:subject, forum_posts_attributes: [:id, :body])
is including the :id necessary?
I can't quite remember. You can try without and see if things like update work correctly and don't create a new forum_post. It might be required so it knows which record to update, but it might not. You may need to play with that to verify.
I just saw this in a comment for the top answer (link at the bottom):
"DON'T FORGET THE ID!!!! pets_attributes: [:id, :name, :category] Otherwise, when you edit, each pet will get created again"
Perhaps this is why. Didn't manage to test it as I'm not enabling edits for my app.
Hi Chris, How can I make it create x amount of posts, specified by the thread starter, each with a different subject, again specified by the thread starter. For example:
New thread Name= Cars
Is there an easier way to do this? what i basically want is kind of a thread within a thread
Hi Chris! I'd like to create a form for a product model, where users can choose a product category first and then can fill the form out. This would be easy, but I'd like to show them different attributes based on the chosen category. Something like if they choose book category, then they will have fields like title, author, published_at, but if they choose shoes category then they can fill out the size, color and type fields. What is the good approach in this case? Should I create more different models like (shoes,books, etc.) or something else? I saw some tuts about dynamic form, but as far as I understand it, I don't need that since the form fields will be predefined and users won't be able to add extra fields.
Hi Chris, great videos! What are your thoughts about using simple_form gem instead of the built in form_for helper? Are there any advantages/disadvantages using this gem?
Simple from gem: https://github.com/platafor...
Pros are you get a lot of helpers for making basic forms quicker, downside is that I often customize my forms a lot so you can't really use their helpers all the time and it's also another API to continuously memorize. I use them in things like Admin areas or forms that don't need much UI work, but other than that, I tend to just use the normal form helpers and tags.
If someone is using Rails 5, notice that "the relational model MUST need the foreign key to create the instance", because the forum_thread has not created yet, the foreign key in forum_post is missing.
In Rails 5, need to add ', optional: true' to forum_post.rb
'belongs_to :forum_thread' >>> 'belongs_to :forum_thread, optional: true'