ViewComponentで作るRailsのみのコンポーネント指向開発
TL;DR
ViewComponentは、Rails6.1からサポートされたHTML を出力する Ruby オブジェクトです。
ViewComponentを用いる事で、ReactやVue.jsを用いる事なく簡易的にSPAライクなアプリケーションかつコンポーネント指向で開発を進めることができます。
今回はNETFLIXのトップページを模写する形で導入手順についてまとめます。
<nav class="w-full fixed z-40">
<div class="px-4 md:px-16 py-6 flex flex-row items-center transition duration-500">
<%= image_tag 'logo.png', class: "h-4 lg:h-7" %>
<div class="flex-row ml-8 gap-7 hidden lg:flex">
<%= component "navbar_item", text: "Home" %>
<%= component "navbar_item", text: "Series" %>
<%= component "navbar_item", text: "Films" %>
<%= component "navbar_item", text: "New & Popular" %>
<%= component "navbar_item", text: "Browse by languages" %>
</div>
</div>
</nav>
環境構築
- Ruby 3.2.2
- Ruby on Rails 7.0.8
rails new
の時点でTailwindCSSを導入しておきます。
rails new netflix_clone --css tailwind
ViewComponent用のGemも必要です。
gem 'view_component'
ViewComponentの使い方
基本的な使い方を追っていきます。
view_component
をインストールすることで、generate
コマンドが使えるようになります。
rails g component コンポーネント名 オブジェクト生成時に渡す引数名
rails g component Example title
を実行することでapp/components
が自動生成され、追加で下記のファイルも作成されます。
# frozen_string_literal: true
class ExampleComponent < ViewComponent::Base
def initialize(title:)
@title = title
end
end
<div>Add Example template here</div>
まずは実際にViewComponent
を呼び出してみます。
rails g controller home top
Rails.application.routes.draw do
root 'home#top'
end
<%= render(ExampleComponent.new(title: "my title")) %>
処理の流れは、
-
views/home/top.html.erb
で<%= render(ExampleComponent.new(title: "my title")) %>
を実行。 -
app/components/example_component.rb
が呼び出され、ExampleComponentクラスのインスタンスを生成。 -
コンポーネント名のスネークケース.html.erb
が呼び出されブラウザに表示。
となっています。
<%= render(ExampleComponent.new(title: "my title")) %>
実行時にインスタンス変数に"my title"
を代入しているので、erb
でも利用することができます。
<%= @title %>
可読性を高める
ViewComponent
を最低限の使い方を追ってきましたが、コンポーネントを利用するたびに<%= render(ExampleComponent.new(title: "my title")) %>
を書くのはあまり直感的ではありません。
その為、オブジェクト生成部分を関数に切り出すように修正していきます。
手順
-
app/components
配下にapplication_component.rb
を作成し、ViewComponent::Base
を継承させる。 -
component()
を定義し、そこからViewComponent
用のオブジェクトを生成。 - 各コンポーネントのrbファイルを
ApplicationComponent
から継承するように修正
class ApplicationComponent < ViewComponent::Base
def component(name, **args, &block)
component = (name.to_s.camelize + "Component").constantize
render(component.new(**args), &block)
end
end
- class ExampleComponent < ViewComponent::Base
+ class ExampleComponent < ApplicationComponent
def initialize(title:)
@title = title
end
end
こうすることで呼び出しをスマートに書くことができます。
- <%= render(ExampleComponent.new(title: "my title")) %>
+ <%= component "example", title: "my title" %>
NETFLIXのトップページを作る
ここから一気にトップページを作っていきます。
まずは必要なコンポーネントを生成しておきます。
# ナビゲーションバー用
rails g component navbar
# ナビゲーションバーの各アイテム用
rails g component navbar_item
# ビルボード(バックグラウンド画像)用
rails g component billboard
ナビゲーションバー
ロゴをapp/assets/images
に配置します。
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-zinc-900 h-full overflow-x-hidden;
}
<%= component "navbar" %>
<nav class="w-full fixed z-40">
<div class="px-4 md:px-16 py-6 flex flex-row items-center transition duration-500">
<%= image_tag 'logo.png', class: "h-4 lg:h-7" %>
</div>
</nav>
次にナビゲーションバーの各リンクを表示させます。
navbar_component.html.erb
内で<%= component "navbar_item", text: "リンク名" %>
を生成するようにします。
<nav class="w-full fixed z-40">
<div class="px-4 md:px-16 py-6 flex flex-row items-center transition duration-500">
<%= image_tag 'logo.png', class: "h-4 lg:h-7" %>
<div class="flex-row ml-8 gap-7 hidden lg:flex">
<%= component "navbar_item", text: "Home" %>
<%= component "navbar_item", text: "Series" %>
<%= component "navbar_item", text: "Films" %>
<%= component "navbar_item", text: "New & Popular" %>
<%= component "navbar_item", text: "Browse by languages" %>
</div>
</div>
</nav>
class NavbarItemComponent < ApplicationComponent
def initialize(text:)
@text = text
end
end
<div class="text-white cursor-pointer hover:text-gray-300">
<%= @text %>
</div>
ビルボード
最後にバックグラウンドで動画を再生させるようにします。
<%= component "navbar" %>
<%= component "billboard" %>
require 'json'
class BillboardComponent < ApplicationComponent
def initialize()
@billboard_item = fetch_billboard_item
end
private
def fetch_billboard_item
file = File.read('billboard.json')
data = JSON.parse(file)
data.first
end
end
[
{
"title":"Big Buck Bunny",
"description":"Three rodents amuse themselves by harassing creatures of the forest. However, when they mess with a bunny, he decides to teach them a lesson.",
"videoUrl":"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
"thumbnailUrl":"https://upload.wikimedia.org/wikipedia/commons/7/70/Big.Buck.Bunny.-.Opening.Screen.png",
"genre":"Comedy",
"duration":"10 minutes"
}
]
<div class="text-white relative h-56.25vw">
<video poster="<%= @billboard_item["thumbnailUrl"] %>"
class="w-full h-[56.25vw] object-cover brightness-[60%] transition duration-500" autoPlay muted loop
src="<%= @billboard_item["videoUrl"] %>"></video>
<div class="absolute top-[30%] md:top-[40%] ml-4 md:ml-16">
<p class="text-white text-1xl md:text-5xl h-full w-[50%] lg:text-6xl font-bold drop-shadow-xl">
<%= @billboard_item["title"] %>
</p>
<p class="text-white text-[8px] md:text-lg mt-3 mb-5 md:mt-8 w-[90%] md:w-[80%] lg:w-[50%] drop-shadow-xl">
<%= @billboard_item["description"] %>
</p>
<div className="mt-3 md:mt-4">
<button class="
bg-white
text-white
bg-opacity-30
rounded-md
py-1 md:py-2
px-2 md:px-4
w-auto
text-xs lg:text-lg
font-semibold
flex
flex-row
items-center
hover:bg-opacity-20
transition
">More Info
</button>
</div>
</div>
</div>
本来であれば、ビルボード内のボタン類もコンポーネントに分けるべきですが、今回はそこまでは考慮しないようにしました。
また、ローカルにJSON
ファイルを配置して読み込むようにしましたが、この部分も実際にAPIを叩くことで勉強になると思います。
まとめ
ViewComponent
と合わせてHotwire
、Stimulus
を組み合わせれば、2割の労力で8割の利益を取りに行くことも実現できるので、今後も追っていきたいです。
最後まで読んでいただきありがとうございました。
参考記事
Discussion