Component Partials in Rails
I presented some thoughts about Rails partials at April’s Brighton Ruby meetup.
The first half discussed various ways to render partials and collections of objects, including using to_partial_path. An example might be rendering a dynamic navigation bar where the navigation items differ depending on the user’s role, e.g. if they’re logged in, or paid, etc. We can create a plain old Ruby object with a to_partial_path method that returns the path of the partial to render. That class can include all the necessary logic, keeping the partial relatively clean:
# app/helpers/navigations_helper.rb
module NavigationsHelper
  def navigation(user)
    Navigation.new(user)
  end
  class Navigation
    def initialize(user)
      @user = user
    end
    def to_partial_path = "navigations/navigation"
    # decides which items to show based on user
    def items
      [Item.new("Home", root_path), …]
    end
    class Item
      #…
      def to_partial_path = "navigations/item"
    end
  end
end<%# app/views/navigations/_navigation.html.erb %>
<nav>
  <ul>
    <%= render navigation.items %>
  </ul>
</nav>Our call to render the navigation is also tidy:
<%# app/views/layouts/application.html.erb %>
<%= render navigation(current_user) %>The second half of the talk demonstrated a derivation of a slotted partial system in around 20 lines-of-code:
module PartialsHelper
  def component_partial
    Partial.new(self)
  end
  class Partial
    def initialize(view_context)
      @view_context = view_context
      @contents = Hash.new { |h, k| h[k] = ActiveSupport::SafeBuffer.new }
    end
    def content_for(name, content = nil, &block)
      if content || block
        content = @view_context.capture(&block) if block
        @contents[name] << content.to_s
        nil
      else
        @contents[name].presence
      end
    end
  end
endWith usage as follows:
<%# app/views/components/_card.html.erb %>
<% yield partial = component_partial %>
<article>
  <div class="…">
    <%= partial.content_for(:image) %>
  </div>
  <h2><%= partial.content_for(:heading) %></h2>
  <div class="…">
    <%= partial.content_for(:description) %>
  </div>
</article><%# app/views/products/_product.html.erb %>
<%= render "card" do |partial| %>
  <% partial.content_for :image do %>
    <%= image_tag product.image %>
  <% end %>
  <% partial.content_for :heading, product.name %>
  <% partial.content_for :description do %>
    <%= simple_format product.description %>
  <% end %>
<% end %>This pattern forms the basis of the nice_partials gem, which includes a ton of extra niceties.