Can a Rails view specify a content_for array so both view and layout are DRY?

572 Views Asked by At

Background: Due to how email clients (mis)handle styles, in a Rails mailer layout, all styles need to be inlined for each tag, so the layout is verbose.

So if you have for example a block of information consisting of 3 paragraphs (%p) in a row with the same style, that style must be applied to each of the five.

# example of  hard_coded_mailer_layout.html.haml
...
%p{style: "some very very long style declaration"}
  This sentence is about Foo.
%p{style: "the SAME REPEATED very very long style declaration"}
  This sentence is about Bar.
%p{style: "yes, again, the SAME repeated long style declaration"}
  This sentence is about FooBar.
...

So now take the case where the text is account-specific and comes from the view (instead of being hard-coded into the layout).

If it's a known maximum number of paragraphs (3 in the above example) the view can simply specify 3 content_for blocks (:foo, :bar, and :foobar) and the layout can have 3 corresponding yields (:foo, :bar, and :foobar) like this:

# example layout_yielding_3_blocks.html.haml
...
- if content_for?(:foo)
  %p{style: "some very very long style declaration"}
    = yield :foo
- if content_for?(:bar)
  %p{style: "the SAME REPEATED very very long style declaration"}
    = yield :bar
- if content_for?(:foobar)
  %p{style: "yes, again, the SAME repeated long style declaration"}
    = yield :foobar
...

# corresponding view
- content_for :foo do
  Your account has 2 Foos.
- content_for :bar do
  Your account has 8 Bars.
- content_for :foobar do
  Your account has 0 FooBars.
...

Question: What about when you want to pass a variable number of paragraphs to the layout, but still have the layout apply the styling? Is there a way to have a view specify an array of N content_for elements, so the layout can simply iterate through them? Specifically something like this:

# desired view... is it possible?
- content_for :info[0] do
  Your account has 2 Foos.
- content_for :info[1] do
  Your account has 8 Bars.
- content_for info[2] do
  Your account has 0 FooBars.
...

so that the layout might look like this:

#  desired corresponding layout, can something like this be done?
...
- yield(:info).each_with_index do |para, i|
  %p{style: "some very very long style declaration"}
    = (yield(:info))[i]

The easy, but problematic way: What is easy to do is have in the view a single content_for that contains all N paragraphs WITH their identical styles repeated N times, like this:

# current way of doing it (bad for 2 reasons below)
- content_for :all_info do
  %p{style: "some very very long style declaration"}
    Your account has 2 Foos.
  %p{style: "the SAME REPEATED very very long style declaration"}
    Your account has 8 Bars.
  %p{style: "yes, again, the SAME repeated long style declaration"}
    Your account has 0 FooBars.

but that stinks (as in code-smelly) because (a) it's very non-DRY and even worse (b) now the inlined styles are spread between the single layout and potentially dozens of views that use that layout... if you change the "style" you need to change it lots of places, or define the style elsewhere as a string or constant named email_para_style_info elsewhere as a variable.

1

There are 1 best solutions below

2
coreyward On

Build and use a view helper instead.

module EmailHtmlHelper
  def account_message(content = nil, &block)
    content_tag :p, content, style: account_message_styles, &block
  end

  def account_message_styles
    %{
      font-size: 1.2em;
      color: #c0ffee;
    }.tr("\n", '')
  end
end

<%= account_message 'Your account has 2 Foos.' %>

<%= account_message do %>
  A longer, 
  more complex
  <span>message</span>
  <%= link_to 'here', foo_url %>
<% end %>

Or if you really don't want styles in a helper, which is a perfectly acceptable place for them to be in this case, you can just tap the styles in your view and use them for each instance:

<% "color: blue;".tap do |inline_styles| %>
  <%= content_tag :p, 'Status: Open', style: inline_styles %>
  <%= content_tag :p, 'Balance: $100', style: inline_styles %>
<% end %>

Note that using tag helpers like content_tag will escape the content appropriately, which means you avoid issues with accidental quotation closing.