View Components を試してみる

先日の LT 会で ViewComponent の紹介をされていて面白そうだったので、試してみます。

ViewComponent は、React インスパイアの Rails 向けのビューコンポーネント構築のフレームワークだそうな。
単体テストができるのも利点なようです。

最終的に、できたのはこちら。

参考

導入

インストール

Gemfile を修正してインストール。

Gemfile
1
gem "view_component", require: "view_component/engine"

コマンドでひな形作成。

1
2
3
4
5
6
7
bash-4.2# bin/rails generate component sidebar
Running via Spring preloader in process 5524
create app/components/sidebar_component.rb
invoke test_unit
create test/components/sidebar_component_test.rb
invoke erb
create app/components/sidebar_component.html.erb

作成されたファイルは以下のようになります。

app/components/sidebar_component.rb
1
2
3
4
5
# frozen_string_literal: true

class SidebarComponent < ViewComponent::Base

end
app/components/sidebar_component.html.erb
1
<div>Add Sidebar template here</div>
test/components/sidebar_component_test.rb
1
2
3
4
5
6
7
8
9
10
require "test_helper"

class SidebarComponentTest < ViewComponent::TestCase
def test_component_renders_something_useful
# assert_equal(
# %(<span>Hello, components!</span>),
# render_inline(SidebarComponent.new(message: "Hello, components!")).css("span").to_html
# )
end
end

実装

適当なページを作って、作った ViewComponent を使ってみます。
ViewComponent の名前の通り、サイドバーを切り出してみます。

呼び出し元のコントローラーなどは今回の主題ではないので、テンプレートから ViewComponent の使用を書いていきます。

views/home/index.html.erb
1
2
3
4
5
6
<main>
<div>メインコンテンツ</div>
</main>
<aside>
<%= render(SidebarComponent.new) %>
</aside>

このような記述で、次のような表示になります。

えっ、サイドバーじゃないじゃん、ダメじゃん。
な見た目ですが、CSS での調整をしていないのでとりあえず表示されていれば OK です。

テスト

さて、独立したテストができるというのがポイントです。
テストを用意してみます。

test/components/sidebar_component_test.rb
1
2
3
4
5
6
7
8
9
10
require "test_helper"

class SidebarComponentTest < ViewComponent::TestCase
def test_component_renders_something_useful
assert_equal(
%(<div>Add Sidebar template here</div>),
render_inline(SidebarComponent.new).css("div").to_html
)
end
end

実行するとこんな様子。

1
2
3
4
5
6
7
8
9
10
bash-4.2# bundle exec rails test test/components/sidebar_component_test.rb
Running via Spring preloader in process 6373
Run options: --seed 30463

# Running:

.

Finished in 0.203129s, 4.9230 runs/s, 4.9230 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

テスト通過できました。

ViewComponent に値を渡す

呼び出し元になるテンプレートから ViewComponent へ値を渡すことができます。

オブジェクトの引数として

views/home/index.html.erb
1
2
3
4
5
6
<main>
<div>メインコンテンツ</div>
</main>
<aside>
<%= render(SidebarComponent.new(user: user)) %>
</aside>
app/components/sidebar_component.rb
1
2
3
4
5
6
7
# frozen_string_literal: true

class SidebarComponent < ViewComponent::Base
def initialize(text:)
@text = text
end
end
app/components/sidebar_component.html.erb
1
2
<div>Add Sidebar template here</div>
<div><%= @text %></div>

動作させると次のようになります。

パラメータとして渡した文字列を反映できました。

with_content の引数として

また、with_content メソッドも使用できます。

views/home/index.html.erb 修正
1
2
3
4
5
6
<main>
<div>メインコンテンツ</div>
</main>
<aside>
<%= render(SidebarComponent.new.with_content("任意のテキスト")) %>
</aside>
app/components/sidebar_component.rb
1
2
3
4
# frozen_string_literal: true

class SidebarComponent < ViewComponent::Base
end
app/components/sidebar_component.html.erb
1
2
<div>Add Sidebar template here</div>
<div><%= content %></div>

この実装でも同じように使用できます。

コントローラーからのレンダリング

コントローラーからのレンダリングもできるそうですが Rails6.1 未満は使えないようです。

turbo frame でのコンテンツ遅延ロードなど使う場合には、コントローラーから ViewComponent のレンダリングが有効に使えそうでした。

コレクションのレンダリング

ここまでは、単一のコンポーネントをレンダリングしましたが、続けてコレクションのレンダリングを試します。

ひな形を作成。

1
2
3
4
5
6
7
bash-4.2# bin/rails generate component item link
Running via Spring preloader in process 6476
create app/components/item_component.rb
invoke test_unit
create test/components/item_component_test.rb
invoke erb
create app/components/item_component.html.erb

作成したひな形を以下のように修正します。

app/components/item_component.rb
1
2
3
4
5
6
7
8
9
10
11
12
# frozen_string_literal: true

class ItemComponent < ViewComponent::Base
# コレクションではこの記述が必要
with_collection_parameter :link

def initialize(link:, link_counter:)
@link = link
# コレクションの index にあたるものを使用できる(テンプレート側では今回は不使用)
@counter = link_counter
end
end
app/components/item_component.html.erb
1
2
3
<li>
<%= link_to @link[:title], @link[:url] %>
</li>

呼び出し側は次のようになります。

views/home/index.html.erb
1
2
3
4
5
6
7
8
9
<main>
<div>メインコンテンツ</div>
</main>
<aside>
<%= render(SidebarComponent.new(text: "任意のテキスト")) %>
<ol>
<%= render(ItemComponent.with_collection([{title: "github",url:"https://github.co.jp/"}, {title:"Amazon", url:"https://www.amazon.co.jp/"}])) %>
</ol>
</aside>

表示すると次のようになります。

リストのレンダリングができました。

本格的にサイドバーを実装してみる

bulma を導入

CSS を 1 から書くのも苦しいので bulma を導入します。

1
gem "bulma-rails", "~> 0.9.1"

実装

以下のように実装しました。

app/views/home/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="container">
<div class="columns">
<div class="column is-9 ">
<h2>メインコンテンツ</h2>
<div>
<p>
吾輩わがはいは猫である。名前はまだ無い。・・・・・
</p>
</div>
</div>
<div class="column is-3 ">
<%= render(SidebarComponent.new) %>
</div>
</div>
</div>
app/components/sidebar_component.rb
1
2
3
4
5
6
7
8
9
10
11
# frozen_string_literal: true

class SidebarComponent < ViewComponent::Base
def initialize
# 本来はテンプレートからデータをもらうところですが、今回はこちらに記述
@items = [
{title: "github",url:"https://github.co.jp/"},
{title:"Amazon", url:"https://www.amazon.co.jp/"}
]
end
end
app/components/sidebar_component.html.erb
1
2
3
4
5
6
7
<aside class="menu">
<p>Links</p>
<ul class="menu-list">
<%= render(ItemComponent.with_collection(@items)) %>
</ul>
</div>
</aside>
app/components/item_component.rb
1
2
3
4
5
6
7
# frozen_string_literal: true

class ItemComponent < ViewComponent::Base
def initialize(link:)
@link = link
end
end
app/components/item_component.html.erb
1
2
3
<li>
<%= link_to @link[:title], @link[:url] %>
</li>

動作させると次のようになります。

何故、コンテンツ本体が『吾輩は猫である』冒頭で、リンク先が github と amazon なのか?はあるものの動作確認できました。


View Component を使ってみました。
Rails】ViewComponent と Partial のパフォーマンスを比較という記事があり、パフォーマンスでも有利な点がありそうです。

今回は取り扱ってませんが、view でしか使わないメソッドを ViewComponent に切り出してあげることができます。
特定の view でしか使わないのに helper に書くよりも見通しがよさそうです。

ではでは。