Search code examples
ruby-on-rails-3collectionspartialsrailstutorial.org

Rails 3 Nested Partials, Collections, Locals (Rails Tutorial 2nd Ed : Chapter 10, Exercise 5)


Working through Michael Hartl's Rails Tutorial 2nd Ed Chapter 10 Exercise 5, I've run into problem with partials and collections and using a partial within a partial.

Chapter 10, Exercise 5 states: "Using partials, eliminate the duplication in the delete links from Listing 10.46 and Listing 10.47."

Listing 10.46 : app/views/microposts/_micropost.html.erb

<li>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
  <% if current_user?(micropost.user) %>
    <%= link_to "delete", micropost, method:  :delete,
                                     confirm: "You sure?",
                                     title:   micropost.content %>
  <% end %>
</li>

Listing 10.47 : app/views/shared/_feed_item.html.erb

<li id="<%= feed_item.id %>">
  <%= link_to gravatar_for(feed_item.user), feed_item.user %>
    <span class="user">
      <%= link_to feed_item.user.name, feed_item.user %>
    </span>
    <span class="content"><%= feed_item.content %></span>
    <span class="timestamp">
      Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
    </span>
  <% if current_user?(feed_item.user) %>
    <%= link_to "delete", feed_item, method:  :delete,
                                     confirm: "You sure?",
                                     title:   feed_item.content %>
  <% end %>
</li>

My approach was to create this file shared/_item_delete_link.html.erb

<%= link_to "delete", item, method:  :delete,
                                 confirm: "You sure?",
                                 title:   item.content %>

Then use this partial in the original partials like this:

Listing 10.46 : app/views/microposts/_micropost.html.erb

<li>
    <span class="content"><%= micropost.content %></span>
    <span class="timestamp">
        Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    </span>
    <% if current_user?(micropost.user) %>
  <%= render partial: 'shared/item_delete_link', collection: @microposts, as: :item %>
    <% end %>
</li>

Listing 10.47 : app/views/shared/_feed_item.html.erb

<li id="<%= feed_item.id %>">
  <%= link_to gravatar_for(feed_item.user), feed_item.user %>
  <span class="user">
    <%= link_to feed_item.user.name, feed_item.user %>
  </span>
  <span class="content"><%= feed_item.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
  </span>
  <% if current_user?(feed_item.user) %>
  <%= render partial: 'shared/item_delete_link', collection: @feed_items, as: :item %>
  <% end %>
</li>

This got all my tests to pass, so I thought it was working until I checked it in the browser: http://grab.by/daUk

The entire collection is being rendered again for each of the _item_delete_link partials, whereas what I wanted was to pass through the local variable from the original collection used in the parent partial.

I tried using the locals: { } and the object: options for render, but had no luck.

Anyone know the answer? Thanks!


Solution

  • Figured it out. You want to pass the locals through to the nested partial, not re-render the collection. To do this, must use the locals: { symbol: :original-local } option (written using the new Ruby hash syntax).

    So, the answer should be this:

    The partial ... app/views/shared/_item_delete_link.html.erb

    <%= link_to "delete", item, method: :delete, confirm: "You sure?",
            title: item.content %>
    

    Listing 10.46 : app/views/microposts/_micropost.html.erb

    <li>
      <span class="content"><%= micropost.content %></span>
      <span class="timestamp">
        Posted <%= time_ago_in_words(micropost.created_at) %> ago.
      </span>
      <% if current_user?(micropost.user) %>
      <%= render partial: 'shared/item_delete_link', locals: { item: micropost } %>
      <% end %>
    </li>
    

    Listing 10.47 : app/views/shared/_feed_item.html.erb

    <li id="<%= feed_item.id %>">
      <%= link_to gravatar_for(feed_item.user), feed_item.user %>
      <span class="user">
        <%= link_to feed_item.user.name, feed_item.user %>
      </span>
      <span class="content"><%= feed_item.content %></span>
      <span class="timestamp">
        Posted <%= time_ago_in_words(feed_item.created_at) %> ago.
      </span>
      <% if current_user?(feed_item.user) %>
      <%= render partial: 'shared/item_delete_link', locals: { item: feed_item } %>
      <% end %>
    </li>
    

    ADDING TO THE PARTIAL: It's possible to refactor this a bit and also include the user conditional into the partial. So, you can make the partial into:

    app/views/shared/_item_delete_link.html.erb

    <% if current_user?(item.user) %>
    <%= link_to "delete", item, method:  :delete, 
           confirm: "You sure?", title: item.content %>
    <% end %>
    

    Then the calls to render must pass in user as well. So, Listing 10.46 : app/views/microposts/_micropost.html.erb

    <%= render partial: "shared/item_delete_link", 
               locals: { item: micropost, user: @user } %>
    

    Listing 10.47 : app/views/shared/_feed_item.html.erb

    <%= render partial: "shared/item_delete_link", 
              locals: { item: feed_item, user: @user } %>
    

    ANOTHER QUESTION: I am confused about when to pass in the object, versus a local variable. In this case, I had to pass in the local variables micropost and feed_item, but the instance variable @user.

    Anyone know why this is true in this case?