ViewComponentReflex触って見たメモ


# これは何？
ViewComponentReflexという「ajaxで値を書き換える処理をRails側だけで実装できてしまうようなもの」を試してみたくなったので、色々試して見たメモ。

https://github.com/joshleblanc/view_component_reflex

## モチベーション
- JavaScriptがあまり好きではないので、画面内のデータの更新だけであればサーバー側だけで書きたかった
- JavaScript + APIで色々やりたい人とは相容れない考え方かもしれない

## 事前知識
ViewComponentReflexは[ViewComponent](https://viewcomponent.org/)の拡張である。
そのため、まずは最低限のViewComponentの知識が必要。

また、ajax的な感じで通信する部分（雑）は [stimulus_reflex](https://github.com/hopsoft/stimulus_reflex) をベースに書かれているが、ざっくりの処理を行うだけだと意識しないで良い形となっている。

---

ここからは、まずはざっくりViewComponentについての記述を行った後、ViewComponentReflexをみていく。

---

# ViewComponentってそもそも何？
## 概略
- Railsで吐き出されるHTMLを、React等々のようにコンポーネント化出来るようにするためのもの
- コンポーネント化すると、Viewのテストも便利だし、データも追いやすい

## partialsと何が違うの？
RailsにはHTMLの一部をコンポーネント化するためのpartialsというものがすでにある。

```:こんな感じのやつ
// 呼び出す側
<div>
  <%= render :partial => "(template_name))" %>
</div>

// 呼び出される側
<p>こいつが表示される</p>

// 結果
<div>
  <p>こいつが表示される</p>
</div>
```

ざっくりいえば下記が違う。

- ViewComponentの方が読み込みが速い
    - 10倍くらい早いと書かれているが、状況次第
- Viewのテストがしやすい
    - Viewのテストコードを書きやすくなる

素直に考えると、partialの進化版みたいなイメージ。

（個人的には...）
GoogleAnalyticsのタグを埋め込むなど、静的HTMLであればpartialの方がシンプル。
一覧等で変数を展開していくのであれば、ViewComponentの方が速いし良い、、位の切り分け。
CSSなどを分けてコンポーネント化しておきたい場合は、変数展開なしでもViewComponentの方が良さそう。

---

ここからはガイドに従ってサンプルを実装した時のメモ。
slimで実装しているので適宜読み替えてほしい。

https://viewcomponent.org/

---


# Installation
Rails6.1だとすんなり入る。
Rails5.0+の場合は [モンキーパッチ](https://github.com/github/view_component/blob/main/lib/view_component/render_monkey_patch.rb)が必要。
先にRails6.1に上げてしまったほうが楽かもしれない。

```
gem "view_component", require: "view_component/engine"
```

# 基本的な動き
ComponentをRubyで定義すると、同じディレクトリにあるHTMLが自動で読み込まれる。
Componentに変数を指定する場合は、`initialize` で指定する。

```app/components/example/component.rb
module Example
  class Component < ViewComponent::Base
    def initialize(title:)
      @title = title
    end
  end
end

```

```app/components/example/component.html.slim
h1 = @title
```

呼び出す時はView側で `render Example::Component.new` してあげる。
変数がない場合はnewだけでよい。

```呼び出し側.slim
= render Example::Component.new(title: 'hogehoge')
```


これくらいをざっと書くと、コンポーネントが展開されたHTMLが出力される。

```出力結果.html
<h1>hogehoge</h1>
```

## コンテンツのエリアを指定したいとき
変数だけでなく、HTMLそのものをコンポーネントに渡したいケースなどは下記のようにする。

### 呼び出し時にブロック要素を渡す方法
ここは上述の通常時と変わらない。
特別な変数を用いなくとも、ブロック要素を`content`に自動で展開してくれるため。

```app/components/example2/component.rb
module Example2
  class Component < ViewComponent::Base
    def initialize(title:)
      @title = title
    end
  end
end
```
ブロック要素を展開したい場所に `content` を配置する。

```app/components/example2/component.html.slim
h1 = @title
div
  = content
```


呼び出し時にブロック要素でHTMLを渡してあげる。

```呼び出し側.slim
= render Example2::Component.new(title: 'hogehoge') do |component|
  h2
    | 色んなHTMLをここに書けるよ
```

ブロック要素が`content`に自動で展開される。

```出力結果.html
<h1>hogehoge</h1>
<div><h2>色んなHTMLをここに書けるよ</h2></div>
```

### コンテンツそれぞれに名前をつける方法
`content` だけでなく、展開したい場所に応じて名前をつけることが出来る。
`header`や`hooter` などを分けて記載したい場合に便利。

`with_content_areas` でコンテンツエリアに名前をつける。

```app/components/example3/component.rb
module Example3
  class Component < ViewComponent::Base
    with_content_areas :header, :footer

    def initialize(title:)
      @title = title
    end
  end
end
```

名前をつけたコンテンツエリアを配置。`content`はいつでも使える。

```app/components/example3/component.html.slim
div
  = header
div
  = content
div
  = footer
```

呼び出し時に`component.with(:コンテンツエリアの名前)`のブロックを作ることで、コンテンツエリア内にその要素が展開される。

```呼び出し側.slim
= render Example3::Component.new(title: 'hogehoge') do |component|
  = component.with(:header) do
    | ここはヘッダーだよ
  = component.with(:footer) do
    | ここはフッターだよ
  h2
    | ここはコンテンツとして展開されるよ
```

`header`や`footer`が対応する場所に展開される。
また、`component.with`で指定しない部分は`content`として展開されるため、h2の内容が真ん中に表示されている。

```出力結果.html
<div>ここはヘッダーだよ</div>
<div>
  <h2>ここはコンテンツとして展開されるよ</h2>
</div>
<div>ここはフッターだよ</div>
```

## Slots
ViewComponentにはSlotsという謎な概念がある。
Slotsを使うと、複数のブロックから成るコンテンツを、ひとつのViewComponentから呼び出すことができる。

ざっくりいえば、呼び出し側は一行だが、ネストしたコンポーネントを扱えるようになる。

### Slotsの種類
一回しか読み込まないか、複数回読み込むかで種類が変わる。

#### renders_one
ヘッダなど、一回しか読み込まれないコンポーネントをslotとして設定するときに使う。

例：`renders_one :header`

#### renders_many
記事へのコメントなど、ひとつの親コンポーネントから複数回呼び出されるコンポーネントに設定する。

例：`renders_many :comments`

### Slotsの呼び出し方
三種類にわかれる。

#### 移譲
シンプルな形だとこれ。

親コンポーネントを定義。

```app/components/example4/component.rb
module Example4
  class Component < ViewComponent::Base
    include ViewComponent::SlotableV2
    
    # 同じクラス内の場合は文字列でコンポーネントを指定
    renders_one :header, 'HeaderComponent'

    # 別ファイルから呼び出す場合はクラスを直接指定
    # postのコンポーネントを複数回呼び出すため、renders_manyを指定している
    renders_many :posts, ExamplePost::Component

    class HeaderComponent < ViewComponent::Base
      attr_reader :title

      def initialize(title:)
        @title = title
      end
    end
  end
end
```

親コンポーネントの中から上記で定義した`header`や`posts`を呼び出すことが出来る。
そうすることで、子コンポーネントがそれぞれ呼び出されるようになる。

```app/components/example4/component.html.slim
div
  / TODO: なぜかhtmlが文字列として展開されてしまうのでhtml_escapeをつけている
  = html_escape header
div
  - posts.each do |post|
    = html_escape post
```

子コンポーネントの中身はそれぞれこのような形。

```app/components/example4/header_component.html.slim
div
  h1 = @title
  div
    = content
```

```app/components/example_post/component.html.slim
h5 = @title
```

```app/components/example_post/component.rb
module ExamplePost
  class Component < ViewComponent::Base
    def initialize(title:)
      @title = title
    end
  end
end
```

呼び出し側からは親コンポーネントを直接呼び出す。
また、`renders_one`, `renders_many` で設定したコンポーネントに呼び出し側から値を設定する。
もちろんここは `@posts.each〜〜`のような形で呼び出し側でループを回しても良い。

```呼び出し側.slim
div
  = render Example4::Component.new do |c|
    = c.header(title: "hogehoge") do
      p ヘッダの中身だよ
    = c.post(title: "1つ目")
    = c.post(title: "2つ目")
```

親コンポーネントを呼び出すだけで、子コンポーネントも勝手に展開されているのがわかる。


```出力結果.html
<div>
  <div>
    <h1>hogehoge</h1>
    <div>
      <p>ヘッダの中身だよ</p>
    </div>
  </div>    
</div>

<div>
  <h5>1つ目</h5>
  <h5>2つ目</h5>
</div>
```

なお、`renders_one` はひとつしか読み込めない。
下記のように複数した場合は後勝ち（つまりはhogehoge2が表示）となる。

```複数のrenders_oneの例.slim
div
  = render Example4::Component.new do |c|
    = c.header(title: "hogehoge1") do
    = c.header(title: "hogehoge2")
```

また、`renders_many`で複数回呼び出す場合は、`posts`のような形で配列を渡すことも出来る。

```
div
  = render Example4::Component.new do |c|
    = c.posts([{title: "1つ目"}, {title: "2つ目"}])
```



#### ラムダ式での呼び出し

移譲のケースは、下記のように書くことも出来る。
間に処理をはさみたいときなどはこちらのほうが便利。

```app/components/example4/component.rb
module Example4
  class Component < ViewComponent::Base
    include ViewComponent::SlotableV2

    renders_one :header, -> (title:) do
      HeaderComponent.new(title: title)
    end

    renders_many :posts, -> (title:) do
      # titleをちょっと変更したいときや、変数を渡して表示内容を制御したいときなどは便利
      ExamplePost::Component.new(title: title + '_hogehoge')
    end

    class HeaderComponent < ViewComponent::Base
      attr_reader :title

      def initialize(title:)
        @title = title
      end
    end
  end
end
```

#### 直接呼び出し


```app/components/example5/component.rb
module Example5
  class Component < ViewComponent::Base
    include ViewComponent::SlotableV2

    renders_one :header
    renders_many :posts
  end
end
```

上記のようにラフに定義だけして呼び出すことも出来る。
コンポーネントの設計が綺麗にできているのであれば、この形がシンプルではある。

```呼び出し側.slim
div
  = render Example5::Component.new do |c|
    = c.header(title: "hogehoge")
    = c.posts([{title: "1つ目"}, {title: "2つ目"}])
```

## インラインコンポーネント
テンプレートを書かずにrbのみでコンポーネントを作ることも出来る。

```app/components/example6/component.rb
module Example6
  class Component < ViewComponent::Base
    # デフォルトでcallという名前が設定されている 
    def call
      link_to 'link to root', root_path
    end

    # call_xxxという名前にした場合、publicなメソッドとして呼び出すことが出来る
    def call_other
      link_to 'link to other', root_path
    end
  end
end

```


```呼び出し側.slim
div
  = render Example6::Component.new
  br
  / with_variant(:other）を指定することで、 call_otherを呼び出している
  = render Example6::Component.new.with_variant(:other)
```

## 条件によってレンダリングするか分けたいとき
`render?` を使うことで制御できる。
これによって、表示制御のロジックをViewから排除することが出来る。

```app/components/example7/component.rb
module Example7
  class Component < ViewComponent::Base
    def initialize(is_show:)
      @is_show = is_show
    end

    def render?
      @is_show == true
    end
  end
end

```

```呼び出し側.slim
div
  = render Example7::Component.new(is_show: true)
  = render Example7::Component.new(is_show: false)
```

```出力結果.html
<div><p>1件だけ表示される</p></div>
```

## レンダリングの前処理

レンダリングする前に前処理を行うことが出来る。
initializeで行えば良い気もするが、複数回ループさせるときなどはこっちのほうが速そう。

```app/components/example8/component.rb
module Example8
  class Component < ViewComponent::Base
    def before_render
      @title = '事前にレンダリングされたタイトル'
    end

    def initialize; end
  end
end
```

## コレクションをパラメーターとして渡したいとき

```app/components/example/component.rb
module Example
  class Component < ViewComponent::Base
    # コレクションの場合に渡されるパラメーター名はデフォルトでは hoge_component.rb のhogeの部分
    # これを変更する場合はwith_collection_parameterとして名前を設定する必要がある
    with_collection_parameter :title

    def initialize(title:)
      @title = title
    end
  end
end

```

```呼び出し側.slim
div
  - titles = ["hoge", "fuga"]
  = render Example::Component.with_collection(titles)
```

## コレクションのカウントを取りたいとき

```app/components/example9/component.rb
module Example9
  class Component < ViewComponent::Base
    with_collection_parameter :title

    # _counterという名前の場合、コレクションをループした回数のカウントが設定される
    def initialize(title:, title_counter:)
      @title = title
      @counter = title_counter
    end
  end
end
```


```呼び出し側.slim
div
  - titles = ["hoge", "fuga"]
  = render Example9::Component.with_collection(titles)

```


# ディレクトリ構造
`app/components`ディレクトリの配下に色々置く。
コンポーネントとしてわかりやすくする観点だと、`app/components/example` 用に、コンポーネント単位でディレクトリを作り、その中に必要なファイルを置く形が良いと考えている。

もちろん、`app/components`直下に色々置く形でも問題ない。

```
app/components
├── ...
├── example
|   ├── component.rb
|   ├── component.css
|   ├── component.html.slim
|   └── component.js
├── ...

// 呼び出す時はフォルダ名をネームスペースとして考慮してくれるので、下記の形となる
<%= render(Example::Component.new(title: "my title")) do %>
  Hello, World!
<% end %>

```

# ViewComponentのテスト

以下はrspecの場合。

## 設定
`render_inline` などの便利メソッドを生やすための設定が必要。

```spec/rails_helper.rb
require "view_component/test_helpers"

RSpec.configure do |config|
  config.include ViewComponent::TestHelpers, type: :component
end
```

## テストの例
Viewのテストが書きやすくなって便利かなと思う。

```ruby
require 'rails_helper'

RSpec.describe Example::Component, type: :component do
  it 'renders something useful' do
    render_inline(described_class.new(title: 'hoge'))
    assert_includes rendered_component, 'hoge'

   # capybaraが入っている時はcapybaraのマッチャも使える
    assert_text('hoge')
  end
end
```

## コンポーネントのプレビュー
`ActionMailer::Preview`のように、コンポーネントのプレビューを見ることが出来る。
個別でCSS書く場合なんかは便利に使えそう。

初期設定として下記が必要。

```config/application.rb
config.view_component.preview_paths << "#{Rails.root}/spec/components/previews"
```

プレビューを見るための設定は下記。
プレビュー用のクラスとメソッドを作成し、そこから呼び出してあげるだけ。

```spec/components/previews/example_preview.rb
class TestComponentPreview < ViewComponent::Preview

  # コンポーネントということを考えると、layoutは設定しない方が良さそう
  # 好みによって設定かな
  layout false

  def default_title
    render(Example::Component.new(title: 'Test component default'))
  end
end

```

上記のファイルの場合はこちらからアクセスできる。
http://127.0.0.1:3000/rails/view_components


---

ここからやっとViewComponentReflexの話に入る。

---

# ViewComponentReflexってそもそも何？
自分もよくわかっていないからこうやって試している。
雑にいえば、ajaxで値を変えたりする何かを、全てサーバー側で記載してしまおうというもの。

# 準備
ActionCableでRedisが必要になるため、何かしらの形でRedisを準備する。
ViewComponentの諸々の設定が終わっていれば、他は特に不要。

# 使い方
```app/components/example10/component.rb
module Example10
  class Component < ViewComponentReflex::Component
    def initialize
      @count = 0
    end

    def increment
      @count += 1
    end
  end
end

```

`refrex_tag`を書いて、コンポーネント側で定義したメソッドを指定するだけで終わり。

```app/components/example10/component.html.slim
= component_controller do
  p
    = @count
  = reflex_tag :increment, :button, "Click"
```

```呼び出し側.slim
div
  = render Example10::Component.new
```

これだけしか書いてないけどサーバー側と通信して数字がインクリメントされている。

![view_component_reflex.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202068/ef0f84f7-a646-efd7-c776-042bf02a7f59.gif)


クリックイベントではなく、マウスエンター等に変更する場合は下記の感じで書く。

```app/components/example10/component.html.slim
= component_controller do
  p
    = @count
  = reflex_tag "mouseenter->increment", :button, "Click"
```

マウスカーソルを合わせるだけで数字が増えるようになる。
![mouseenter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202068/7b2d11a1-0ffd-ad31-9916-5a9d680f5f10.gif)


## 他の要素を動かしたいとき

component_controllerを利用すると、変数keyが自動で生成される。
こののkeyを利用すると、他のコンポーネントの処理を動かすことが出来るようになる。


```parent.slim
= component_controller do
  div#loader
    - if @loading
      p ロード中
  = reflex_tag :do_action, :button, "Click"
  / component_controllerによってkeyが自動生成されている
  = render Child::Component.new(parent_key: key)
```

```parent.rb
module Parent
  class Component < ViewComponentReflex::Component
    def initialize
      @loading = false
    end

    # 子から呼ばれる処理
    def update_loading
      @loading = !@loading
      
      # コンポーネント内の特定のセレクタを更新する処理
      # prevent_refresh!という、更新を受け付けない処理もある
      # refresh_all!すると、body要素全てを更新する
      refresh! '#loader'
    end
  end
end
```

```child.rb
module Child
  class Component < ViewComponentReflex::Component
    def initialize(parent_key:)
      @parent_key = parent_key
    end

    def stimulate_parent
      # 親の処理をstimulateで叩く
      # parent_keyでコンポーネントの紐付けを行っている
      stimulate("Parent::Component#update_loading", { key: @parent_key })
    end
  end
end
```

## コレクションに要素を追加したいとき
要素のコレクション自体を保持する親要素と、要素のコレクションを表示する子要素に分けて実装する。

```parent.rb

class MyUserModel
  attr_accessor :id, :name

  def initialize(id:, name:)
    @id = id
    @name = name
  end
end

module Parent

  class Component < ViewComponentReflex::Component

    def initialize(users:)
      @users = users
    end

    def add_user
      # 普通は外部からデータを渡すと思うが、サンプルなので。
      # ViewComponentの中でどこまでやるかは悩ましい。データ保存からの再表示等々もやることもあるとは。
      @users.append(MyUserModel.new(id: @users.length + 1, name: "#{(@users.length + 1).to_s}郎"))
    end
  end
end
```

```child.rb
module Child
  class Component < ViewComponentReflex::Component
    # これを定義し忘れるとうまく引数を読み込まない￥
    with_collection_parameter :user

    def initialize(user:)
      @user = user
    end

    # こいつを設定しておかないと怒られる
    def collection_key
      @user.id
    end
  end
end
```

```parent.slim
= component_controller do
  / コレクションを表示するコンポーネント（子）を呼び出し
  = render Child::Component.with_collection(@users)
  = reflex_tag :add_user, :button, "太郎を増やす"
```

```child.slim
/ 表示しているだけ
= component_controller do
  = @user.id
  = @user.name
```

```呼び出し元.slim
div
  = render Parent::Component.new(users: [MyUserModel.new(id:1, name:'1郎')])
```

あとはこれらを応用していくだけ。

# 雑感
多少癖はあるし、まだまだ足りない部分は多いが、軽めのサービスだったらトライしてみて良さそう。
掲示板等のコメント投稿後のリロード等々だと使い勝手は良さそう。
データの更新等々を誰がやるべきかは難しい。ベストプラクティスはわからない。