Search code examples
ruby-on-railsruby-on-rails-4renderpartial

Strange behaviour with .build


Background

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.

Question

  1. How do I fix it? (solved)
  2. Why does changing the order of the form and the comments cause the 'bug' to disappear?

View

#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 %>

Partials

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 %>

Summary of the answers/findings

Solution

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

Comparison of 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

EDIT

Stated questions more explicitly
added summary of answers/discussion


Solution

  • <%= 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).