Rails view helpers, custom block methods & RSpec testing
By: Ben Stacey
Tags: Engineering, Rails, Design Patterns, Rspec, Ruby

In collaboration with the Biggerpockets’ design & UX team we have recently deployed code to support our first shared component as the first part of a shared ‘design system’.

The aim was to write a Rails helper method with a block, that would return both passed-in content with specific attributes, as well as pre-defined attributes to support stylesheet declarations and Javascript handlers.

Similar to form builder objects used in conjunction with form_for / form_with , we wanted to allow passing link_to like helpers, with predefined body url and HTML attribute arguments, and have these render as <a> elements with a set of additional HTML attributes added, within a fixed element hierarchy.

We wanted:

<%= overflow_helper do |d| %>

<%= d.link("Edit listing", edit_listing_path(listing)) %>

<%= d.link("Delete Listing", admin_listing_path(listing), class: "overflow__list-item-danger") %>

<% end %>

to return:

<!-- Further parent elements omitted --> <ul

class="j-overflow__list-container"

role="list"

tabindex="0"

aria-hidden="true"

aria-labelledby="marketplace-listing-edit-overflow-toggle"

data-action="click@overflow#hide"

data-target="janus--overflow.listContainer"

>

<li class="overflow__list-item-wrapper">

<a

class="overflow__list-item"

href="/real-estate-listings/listing/edit"

tabindex="0"

data-action="keydown->overflow#keyboardToggle"

data-target="overflow.listItem"

>Edit Listing</a>

</li>

<li class="overflow__list-item-wrapper">

<a

class="overflow__list-item overflow__list-item-danger"

href="#"

tabindex="0"

data-action="keydown->overflow#keyboardToggle"

data-target="overflow.listItem"

>Archive Listing</a>

</li>

</ul>

Helper method expressions in block

Writing the initial overflow method body is trivial with Rails’ content helpers. Being able to define additional methods that can be supplied to the block, like link less so. Here is how we did it:

def overflow(&block)

# Parent element building content expressions omitted block.call(OverflowHelpers.new) if block.present?

end

Pass the class, which possesses the required helper method(s) as the first argument to call . This sets the d variable in the example above to an instance of OverflowHelpers , so a link method can be called on it and arguments passed.

It is then simply a case of defining the new link method with further content_tag methods as needed:

class OverflowHelpers def link(label, path, **attributes)

content_tag(:li, class: "overflow__list-item-wrapper") do

content_tag(

:a,

label,

build_link_attributes(path, attributes),

)

end

end private def build_link_attributes(path, attributes)

{

href: path,

tabindex: "0",

class: "overflow__list-item",

data: {

target: "overflow.listItem",

action: "keydown->overflow#keyboardToggle"

}

}.deep_merge(attributes) do |_, old_value, new_value|

"#{old_value} #{new_value}"

end

end

Note the deep_merge which allows attributes given at the overflow helper method call site to be concatenated with those pre-defined in the helper itself. This is especially common/useful when you need to pass specific styling classes, and have them co-exist with those defined by the helper method itself; the overflow__list-item-danger class in this case.

Automated testing of ERB helper methods

Beyond writing Rails system specs, we came up with a nice way to test methods like overflow and their respective helpers. We wanted to avoid view specs, but still be able to assert rendered markup in a flexible and easy to read manner.

First we wrote the implementation of the view helper as a HEREDOC:

let(:erb) do

<<-FRAGMENT

<%= janus.overflow do |d| %>

<%= d.link('Survey', '/path-to-survey', data: { action: 'click->test#trackClickEvent' }) %>

<% end %>

FRAGMENT

end

We then employed an instance of Capybara::Node::Simple as a thin wrapper around the rendered markup, and to provide the usual parsing and testing functionality. The idea for this was taken from this excellent blog post:

https://thoughtbot.com/blog/use-capybara-on-any-html-fragment-or-page

It should then be possible to set a node variable as follows:

let(:node) do

Capybara::Node::Simple.new(erb)

end

Before writing expectations like this:

it "renders a labelled link to the passed target" do

expect(node).to have_link(

"Survey",

href: "/path-to-survey",

count: 1,

)

end

However, you will run into errors like undefined method: content_tag because the Rails helper methods will not be in scope. In order to have Rails’ content_tag and related methods available in these specs, we needed to pass in an instance of the default ActionView class as follows:

let(:context) { Application.new.view_context }

and amend our Capybara::Node::Simple expression:

let(:node) do

Capybara::Node::Simple.new(context.render(inline: erb))

end

The more complete spec file example: