Rails View Helpers for Components
December 23, 2024

Rails View Helpers for Components

The purpose of this post is to collect feedback on new Rails feature proposals, and I’ve put together a Pull Request. I welcome any comments, either here or on X or BlueSky


Translation: Ph.D.

I’d like to propose an extension to Ruby on Rails TagHelpers that will allow us to create HTML structures that were previously cumbersome.

In short, the view helper code would be written like this:

src="/path/to/image.jpg" alt="Alternate Text" />
Figure Caption
Enter full screen mode

Exit full screen mode

It might look like this (with my extension proposal),

def figure(src, alt:, figcaption: nil)
  tag.figure do |b|
    b << tag.img(src:, alt:)
    b << tag.figcaption(figcaption) if figcaption.present?
  end
end
Enter full screen mode

Exit full screen mode

Instead of this (how we currently write this using view helpers),

def figure(src, alt:, figcaption: nil)
  figure_content = tag.img(src:, alt:)
  figure_content += tag.figcaption(figcaption) if figcaption.present?
  tag.figure do
    figure_content
  end
end
Enter full screen mode

Exit full screen mode

The proposed approach allows us to write more complex HTML using only tag helpers (without ERb), using code that closely reflects the structure of HTML. This will simplify the creation of UI components.

Note: The HTML and view helper code snippets above are from Garrett Dimon’s blog post.


Thanks to Garrett Dimon and Adam McCray

Garrett Dimon wrote a great article “Build your ERb and Partials for more maintainable front-end code in Rails” In it he discusses alternatives to using view helpers and partial views, as well as key points to consider when choosing between the two.

Adam McCrea wrote an article in response, “Say no to Partials and Helpers for a maintainable Rails frontend”proposed “to completely abandon the ERB part and helpers, and use Phlex instead”.

While I completely agree that Phlex is awesome and refreshing, I hope my proposal addresses at least one of Adam’s main complaints and allows us to actively re-evaluate traditional Rails ERb and view helpers.

In short, I don’t want to throw out the baby with the bathwater.


Let’s take a look at ViewComponents and Phlex first

Before delving into the details of my proposal, let’s take a look at how to write the above HTML using ViewComponents and Phlex.

I hope this illustrates my point, which can be summarized as “Why do we need to write a new category for 4 lines of HTML?”.

I think there is a better way.


view element

graph component.rb

class FigureComponent < ViewComponent::Base
  def initialize(src, alt:, figcaption: nil)
    @src = src
    @alt = alt
    @figcaption = figcaption
  end
end
Enter full screen mode

Exit full screen mode

graph component.html.erb

src="<%= @src %>" alt="<%= @alt %>" /> <% if @figcaption.present? %>
<%= @figcaption %> <% end %>
Enter full screen mode

Exit full screen mode


Flex

class Components::Figure < Components::Base
  def initialize(src, alt:, figcaption: nil)
    @src = src
    @alt = alt
    @figcaption = figcaption
  end

  def view_template
    figure do
      img(src: @src, alt: @alt)
      figcaption { @figcaption } if @figcaption
    end
  end
end
Enter full screen mode

Exit full screen mode


this proposal

def figure(src, alt:, figcaption: nil)
  tag.figure do |b|
    b << tag.img(src:, alt:)
    b << tag.figcaption(figcaption) if figcaption.present?
  end
end
Enter full screen mode

Exit full screen mode


Need a simpler way

Both ViewComponent and Phlex are class-based, making them ideal for components that need to encapsulate logic.

However, with the rise of Tailwind CSS, we were encouraged to write many smaller components whose sole purpose was to dry out our code and save us from writing tons of CSS classes – which had little to no internal logic. In these cases, ViewComponents and Phlex may be overkill.

In many cases, the traditional view helpers included with Rails are sufficient for this purpose. For example, for the following HTML, you can write a very easy-to-understand helper that mirrors the HTML structure as shown below.

hypertext markup language

src="/path/to/image.jpg" alt="Alternate Text" />
Enter full screen mode

Exit full screen mode

View Assistant

def figure(src, alt:)
  tag.figure do
    tag.img(src:, alt:)
  end
end
Enter full screen mode

Exit full screen mode

However, when

Element has multiple child elements.

For the following HTML, where

With two children, you can write a view helper that’s hard to read, or more commonly, you can use ERb.

Although I fully understand that ERb is better than the view helper at rendering highly complex HTML, I find it difficult to accept the idea that simply increasing the number of children from 1 to 2 will make the HTML more complex. I’m uncomfortable with the idea that this alone forces us to use ERb instead of view helpers.

hypertext markup language

src="/path/to/image.jpg" alt="Alternate Text" />
Figure Caption
Enter full screen mode

Exit full screen mode

View Assistant (difficult to read)

def figure(src, alt:, figcaption: nil)
  figure_content = tag.img(src:, alt:)
  figure_content += tag.figcaption(figcaption) if figcaption.present?
  tag.figure do
    figure_content
  end
end
Enter full screen mode

Exit full screen mode

ERb (you often use this to replace the view helper above)

src="<%= src %>" alt="<%= alt %>" /> <% if figcaption.present? %>
<%= figcaption %> <% end %>
Enter full screen mode

Exit full screen mode

Given this situation, I came up with the idea of ​​making it easier to write HTML elements with multiple child elements in a view helper by adding a markup helper feature.


Why doesn’t tag helper work when there are multiple children?

If we write the following code in the view helper, we will lose tags and get only

.

This is because the value returned do Blocks used as content

Label. since tag.figcaption is the last expression in the block, and that value becomes the content. this tag.img Lost because its value is not captured anywhere.

View Assistant

def figure(src, alt:, figcaption: nil)
  tag.figure do
    tag.img(src:, alt:)
    tag.figcaption(figcaption) if figcaption.present?
  end
end
Enter full screen mode

Exit full screen mode

hypertext markup language

Figure Caption
Enter full screen mode

Exit full screen mode


Suggested solution

I recommend using the following syntax. We extend the tag helper to optionally provide variables (in this case b for “buffer”) to block. we promote tag.img and tag.figcaption to this buffer (b), which is implemented as an array. After the tag helper generates the chunk, it executes safe_join Produces HTML containing two child elements on the buffer and will be used as

Label. This behavior is only triggered when the number of blocks is 1 or greater, ensuring backwards compatibility. (The current implementation of tag helper does not use block variables)

def figure(src, alt:, figcaption: nil)
  tag.figure do |b|
    b << tag.img(src:, alt:)
    b << tag.figcaption(figcaption) if figcaption.present?
  end
end
Enter full screen mode

Exit full screen mode

The resulting code is tightly integrated with the structure of HTML and is easy to compare. Even someone with little knowledge of Ruby should be able to understand what’s going on.


benefit

With this proposed feature, developers who are hesitant about using ViewComponents or Phlex can better benefit from components while sticking with traditional ERb and view helpers.

It also provides a mechanism for creating widgets using functions (view helpers) that is easy to understand for non-Ruby developers (that is, designers). Note that React elements are also functions, not classes.

This feature allows us to implement slightly more complex elements in the View Assistant without having to resort to ERb templates. Compared with ERb, the advantages are the ability to accommodate multiple components in one file, explicit interface, simpler API calls (no need to write render "[path to partial]", [hash of locals] This can get verbose) and performance.


What about unit testing?

One of the common benefits of using ViewComponents or Phlex instead of ERb and view helpers is the ability to unit test components.

I don’t know where this idea came from, but I do know that the Rails guide has something to do with Unit test ERb part And for Unit Test View Assistant.


implement

Although I’ve been using variations of this technique for years, I recommend adding it to Rails to make it more convenient and raise awareness of the technique. safe_join technology.

I’m currently preparing the implementation and pull request. In the meantime, if you have any comments or comments, I’d love to hear them!

2024-12-23 01:00:32

Leave a Reply

Your email address will not be published. Required fields are marked *