What is the purpose of the rails `capture` helper?

From the Documentation:

The capture method extracts part of a template as a string object. You can then use this object anywhere in your templates, layout, or helpers.

The capture method can be used in ERB templates…

<% @greeting = capture do %>
  Welcome to my shiny new web page!  The date and time is
  <%= Time.now %>
<% end %>

…and Builder (RXML) templates.

@timestamp = capture do
  "The current timestamp is #{Time.now}."
end

You can then use that variable anywhere else. For example:

<html>
<head><title><%= @greeting %></title></head>
<body>
<b><%= @greeting %></b>
</body>
</html>

The return of capture is the string generated by the block. For Example:

@greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500"

But why would I need it? The use case above, seems super contrived?

Would be interested in people’s thoughts?

There are two situations where I use capture, and they are both when I’m building reusable components in view helpers, where I need to write ruby code.

Example 1: Part of the component is defined where it will be used.

Here I’m building a modal component using the details html element which already provides a feature to “open when clicked”. I’ve simplified the code a bit and fudgeball is the name of our design system.

In this example, the helper expects the view to supply it with a block that will contain what should go inside the modal when it is open. To do this it uses capture to convert the block into a string that can be concatenated through concat into the details tag.

def fudgeball_modal(key = nil, options = {}, html_options = {}, &block)
  content_tag "details", html_options do
    concat fudgeball_button(key, option)
    concat capture(&block)
  end
end
<%= fudgeball_modal :edit do %>
  <p>I become visible when the modal is open</p>
<% end %>

Example 2: Your component does not have a parent element but is made of many elements

Here I’m building a component that allows a form to be split into multiple pages. Normally you would use content_tag "div" and put all the elements inside its block (like I did in the example above), but I did not want to have an empty div in my html just so I could use its block. Fortunately capture provides the same functionality without creating the reduntant tag.

def fudgeball_pages(title, options = {}, &block)
  capture do
    concat(render "page_start", title: I18n.t(title, scope: :pages), &block)
    options[:pages].each_with_index do |page, index|
      concat(render "page_numbered", title: I18n.t(page[:title], scope: :pages), index: index, content: page[:content])
    end
  end
end
3 Likes

I’ve also used it when needing to inject markup into some sort of data attribute for JS to pickup. For example Bootstrap popovers can allow HTML. So I might do something like:

<% popover = capture do %>
  My <strong>fancy</strong> markup.
<% end %>
<button type="button" data-bs-toggle="popover" data-bs-content="<%= popover %>">Show my popover</button>

(that is off the top of my head so there but I know I have done something conceptually like that before).

1 Like

Hi Breno thanks for your examples. much appreciated.

Example 2: Your component does not have a parent element but is made of many elements

Here I’m building a component that allows a form to be split into multiple pages. Normally you would use content_tag "div" and put all the elements inside its block (like I did in the example above), but I did not want to have an empty div in my html just so I could use its block. Fortunately capture provides the same functionality without creating the reduntant tag.

def fudgeball_pages(title, options = {}, &block)
  capture do
    concat(render "page_start", title: I18n.t(title, scope: :pages), &block)
    options[:pages].each_with_index do |page, index|
      concat(render "page_numbered", title: I18n.t(page[:title], scope: :pages), index: index, content: page[:content])
    end
  end
end

re: Example 2 - I could not follow / understand the the reasoning.

Normally you would use content_tag "div" and put all the elements inside its block (like I did in the example above), but I did not want to have an empty div in my html just so I could use its block.

How does using capture obviate this issue?