Dom Christie

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
end

With 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.