I was learning from rails guide when I encountered this queer behaviour cause by build
So in the guide I was making a blog where posts had comments section. In the guide they made the comments posted appear before the comments form for adding new comments. Somehow I wanted to tried the other way around (comments form first). However when I did that, additional empty tags <h4></h4><p></p>
were rendered.
Initially I thought it was rendering an empty comment from the model but after running
<%= @article.comments.count %> # => 2 gives expected comments count
Now comes the queer part. When I inverted the order as per the guide, comments form first then comments, the empty tags disappeared and everything works fine.
#This works
#comments
<h3>Comments</h3>
<%= render @article.comments %>
#comments form
<h3>Add a comment!</h3>
<%= render 'comments/form' %>
#But not this
#comments form
<h3>Add a comment!</h3>
<%= render 'comments/form' %>
#comments
<h3>Comments</h3>
<%= render @article.comments %>
comment partial
<h4>
<%= comment.commenter %>
</h4>
<p>
<%= comment.body %>
</p>
comment form partial
<%= form_for([@article, @article.comments.build]) do |f| %>
<p class="commenter">
<%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p class="text">
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
Comment.new
is used instead of @article.comments.build
as .build
is making an extra instance (is this a bug?)
new
vs build
According to Kris
.new method has changed since Rails > 3.2.13
new
and build
are the same. build
is merely an alias
size
vs count
I found this SO post about count vs size and the recommended reading in that post. In case someone passes by and wants to know more about the subtlety.
In essence (from the SO post) and @Jiří Pospíšil answer
count
sends a query to the db to retrieve the number of elements. In this context @article.comments.count returns the number of comments in the DB
length
give the number of comments loaded into memory that said, memory and db data might not be the same. Some elements in the memory might be new.
size
as @Jiří Pospíšil will give the number of elements in the collection if it has been loaded (like length
) else works like count
and sends a SQL COUNT query
#when .build was used
<%= @article.comments.length %> # => 2
<%= @article.comments.count %> # => 1
<%= @article.comments.size %> # => 2
and when the proposed solution Comment.new
was used, all methods return 1 which is consistent with what this guy said
Stated questions more explicitly
added summary of answers/discussion
<%= form_for([@article, @article.comments.build]) do |f| %>
The @article.comments.build
part will create a new Comment
and add it to the @article.comments
collection. Later on, you iterate over the collection and that's why there's one more than should be. You can actually see it happening if you do something like this.
<%= form_for([@article, @article.comments.build(commenter: "Hello!")]) do |f| %>
To get around the issue, you need to create a Comment
but not associate it with the collection. Using Comment.new
instead of @article.comments.build
should be enough as the record itself is not important.
Note that the reason you're seeing the right number of comments (@article.comments.count
) is because the comment created via build
hasn't been saved to the database and #count
always perform a COUNT
query regardless of whether the collection has already been loaded. You could see the extra comment by using #size
instead (@article.comments.size
).