Simplify Rails Views Using ViewComponents with Tailwind CSS and RSpec
Tags: ruby, rails, tailwindcss, rspec
Published: 2022-02-20 06:51:05

I recently did a [deep dive](https://stefcoetzee.com/2022/02/18/rails-viewcomponent-rspec-tailwind) on refactoring Rails views to reusable view components.

I thought I’d share some of the components that were extracted from the example application used for this exercise with you here.

You can view the code for the refactored application in this [repo](https://github.com/stefcoetzee/rails-viewcomponent-rspec-tailwind).

## `LinkComponent` and its children

To cut down on duplicate link styling, I created a `LinkComponent` template:

```erb
<%# app/components/link_component.html.erb %>

<div class="text-gray-600 transition duration-300 border-b border-white max-w-fit hover:border-b hover:border-gray-400">
  <%= content %>
</div>

```

Since we’re just rendering styled content here, the associated spec is super simple:

```ruby
# spec/components/link_component_spec.rb

require "rails_helper"

RSpec.describe LinkComponent, type: :component do
  it "renders text" do
    test_text = "some text"
    render_inline(described_class.new.with_content(test_text))

    expect(rendered_component).to have_text(test_text)
  end
end

```

More specific link templates simply inherit from this base template. I decided to leave the `link_to` methods in the various link components that inherit from `LinkComponent` so that `DeleteComponent` can also be part of the family.  Before we take a look at the various link components’ code, here they are in action in the show team view (`app/views/teams/show.html.erb`):

![show team view with link components outlined](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aqp5b3kr0lvs3mm000f4.png)

Four separate link templates are used in the view above. One each for:

- Back navigation out of the show view,
- Editing a team,
- Deleting a team, and
- Adding a player to the team.

All of their templates and specs are provided below, should you want to make use of something similar in your own projects.

### Back navigation

Template:

```erb
<%# app/components/back_navigation_component.html.erb %>

<%= render LinkComponent.new do %>
  <%= link_to @href do %>
    <div class="flex items-center space-x-1">
      <svg class="w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
          xmlns="http://www.w3.org/2000/svg">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
              d="M10 19l-7-7m0 0l7-7m-7 7h18">
        </path>
      </svg>
      <div>
        Back to <%= @text %>
      </div>
    </div>
  <% end %>
<% end %>

```
 
Spec:

```ruby
# spec/components/back_navigation_component_spec.rb

require "rails_helper"

RSpec.describe BackNavigationComponent, type: :component do
  it "renders a back-navigation link" do
    test_url = "https://example.com"
    test_text = "team"
    render_inline(described_class.new(href: test_url, text: test_text))

    expect(rendered_component).to have_link(
      "Back to #{test_text}",
      href: test_url
    )
  end
end

```

### Edit link

Template:

```erb
<%# app/components/edit_link_component.html.erb %>

<%= render LinkComponent.new do %>
  <%= link_to @href do %>
    <div class="flex items-center space-x-1">
      <svg class="w-4" fill="none" stroke="currentColor"
        viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
              d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z">
        </path>
      </svg>
      <div>
        Edit <%= @text %>
      </div>
    </div>
  <% end %>
<% end %>

```

Spec:

```ruby
# spec/components/edit_link_component_spec.rb

require "rails_helper"

RSpec.describe EditLinkComponent, type: :component do
  it "renders an edit link" do
    test_url = "https://example.com"
    test_text = "player"
    render_inline(described_class.new(href: test_url, text: test_text))

    expect(rendered_component).to have_link "Edit #{test_text}", href: test_url
  end
end

```

### Delete link

Template:

```erb
<%# app/components/delete_link_component.html.erb %>

<%= render LinkComponent.new do %>
  <%= link_to @href, data: {
      turbo_method: :delete,
      turbo_confirm: "Are you sure?"
  } do %>
    <div class="flex items-center space-x-1">
      <svg class="w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
          xmlns="http://www.w3.org/2000/svg">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
              d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
        </path>
      </svg>
      <div>
        Delete <%= @text %>
      </div>
    </div>
  <% end %>
<% end %>

```

Spec:

```ruby
# spec/components/delete_link_component_spec.rb

require "rails_helper"

RSpec.describe DeleteLinkComponent, type: :component do
  it "renders a delete link" do
    test_url = "https://example.com"
    test_text = "player"
    render_inline(described_class.new(href: test_url, text: test_text))

    expect(rendered_component).to have_link "Delete #{test_text}", href: test_url
  end
end

```

### Add link

Template:

```erb
<%# app/components/add_link_component.html.erb %>

<%= render LinkComponent.new do %>
  <%= link_to @href do %>
    <div class="flex items-center space-x-1">
      <svg class="w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
          xmlns="http://www.w3.org/2000/svg">
        <path stroke-linecap="round"
              stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6">
        </path>
      </svg>
      <div>
        Add a <%= @text %>
      </div>
    </div>
  <% end %>
<% end %>

```

Spec:

```ruby
# spec/components/delete_link_component_spec.rb

require "rails_helper"

RSpec.describe AddLinkComponent, type: :component do
  it "renders an add link" do
    test_url = "https://example.com"
    test_text = "player"
    render_inline(described_class.new(href: test_url, text: test_text))

    expect(rendered_component).to have_link "Add a #{test_text}", href: test_url
  end
end

```

## A componentized page header

Once defined, component templates can easily be called. Here they are as part of the page header in the image up above:

```erb
<%# app/views/teams/show.html.erb %>

<div class="mb-2">
  <%= render PageHeaderComponent.new do %>
    <div class="mb-1">
    <%= render BackNavigationComponent.new(
          href: teams_path,
          text: "teams"
    ) %>
    </div>
    <div class="mb-1">
      <%= render PageTitleComponent.new(title: @team.name) %>
    </div>
    <div class="flex space-x-3">
      <%= render EditLinkComponent.new(href: edit_team_path(@team), text: "team") %>
      <%= render DeleteLinkComponent.new(href: team_path(@team), text: "team") %>
    </div>
  <% end %>
</div>
...
```

As you might expect, the `PageHeaderComponent` and `PageTitleComponent` templates are super simple:

```erb
<%# app/components/page_header_component.html.erb %>

<div class="pb-2 border-b">
  <%= content %>
</div>

```

```erb
<%# app/components/page_title_component.html.erb %>

<div class="text-xl font-bold">
  <%= @title %>
</div>

```

That’s the beauty of working with ViewComponent: define once, use anywhere.

If you enjoyed this brief look at ViewComponent templates, you might enjoy the [full post](https://stefcoetzee.com/2022/02/18/rails-viewcomponent-rspec-tailwind). Check out the `start-here` git branch and follow along from the comfort of your own editor as we refactor a fully-tested example application to reusable components.