コメントした人のアバターを並べて表示するためのコード

## この記事の内容
デイトラWeb開発コースの課題で以下のようなものがあり、結構手間取ったので実装した方法を残します。

![スクリーンショット 2024-09-01 215629.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3875060/d12a6c00-8d03-08d0-cae8-aedea1433b88.png)


### 使っている技術とバージョン
- Rails 6.0.6.1
- ruby 2.7.7
- scss

### モデルの関係性
- UserモデルにTaskモデルが1対Nの関係で紐づいている
- Userモデルにavatarの画像が保持されている
- CommentsモデルはUserモデル、Taskモデルとそれぞれ1対Nの関係で紐づいている
- UserモデルにはBoardモデルも1対Nの関係で紐づいており、BoardモデルにはTaskモデルが1対Nの関係で紐づいている 

### 得たい成果
- `views/tasks/index.html.haml`上に記事の作成者とコメントした人のアバターを重ねて表示したい

### ポイント
- ユーザーをユニーク化して表示する必要がある
    - コメントごとにアバターを表示すると、同じユーザーのAvatarが複数回出てしまうため
- showではなくindex画面で表示する必要がある
    - showだとtaskが一意に決まるので簡単
    - indexだとtasksの配列から要素を取り出して設定する必要があるため手数が増える

## 実装したコード
### task_controller
```
  def index
    @tasks = Task.where(board_id: params[:board_id])
    @users_with_avatars = {}
    
    @tasks.each do |task|
      @users_with_avatars[task.id] = avatars(task)
    end
  end

  private
  def avatars(task)
    avatars = []
    avatars << task.user.id

    comments = task.comments.all
    comments.each do |comment|
      if !avatars.include?(comment.user.id)
        avatars << comment.user.id
      end
    end

    avatars
  end
```

### tasks/index.html.haml
```
.card_avatars
  - @users_with_avatars[task.id].each do |avatar|
    - if User.find(avatar).avatar.attached?
      = image_tag task.user.avatar, class: 'card_avatar'
    - else
      = image_tag 'default-avatar.png', class: 'card_avatar'
```

## 詳細
### task_controller
```
  private
  def avatars(task)
```
- avatarsメソッドを定義
- indexアクション内での使用のみを想定しているのでpr-ivateで設定
- indexアクション内で@taskを引数にするために(task)を記載
```  
    avatars = []
```
- 後にビュー側でeachメソッドで吐き出せるように配列として定義
- ユニーク化するため、user.idの配列を取得して入れていく
```
    avatars << task.user.id
```
- タスクの作成者のアバターは必ず入るため`avatars`に追加
- 定義したときに入れてもいいけど、わかりやすさを重視するため別行に記載
```
    comments = task.comments.all
    comments.each do |comment|
```
- taskに紐づいたcommentをすべて取得
```
      if !avatars.include?(comment.user.id)
```
- コメントした人のユニーク化のための記述
- これがないと、一人の人が複数回コメントすると複数個の同一人物のアバターが表示される
- includeメソッド：配列内に指定した要素があればture、なければfalseを返すメソッド
- `avatars.include?(comment.user.id)`と記述すると配列`avatars`内に`comment.user.id`（コメントした人のユーザーID）があればture、なければfalseを返す
- 今回の場合、`avatars`の中にすでに同じuser.idがあればfalseにしたいので、`!`をつけて論理式を逆にする

```
        avatars << comment.user.id
      end
    end
```
- avatars.include?(comment.user.id)がfalseだった場合＝`avatars`内にそのuser.idが含まれない場合、`avatars`にuser.idを追加する

```
    avatars
  end
```
- これ忘れがち
- メソッドは最後の行の記述をreturnとして返すから、これがないと配列`avatars`を取得できない

```
  def index
    @tasks = Task.where(board_id: params[:board_id])
```
- もともと記載していた内容
- ビュー側で`@tasks.each do`を使って全タスクを表示するため記載していた

```
    @users_with_avatars = {}
```
- ハッシュを作る
- このあとに`@tasks`から`each do`で取得したtaskを使って、keyにtaskのid、valueに先ほど定義したavatarsメソッドで作る配列とするため
- このハッシュがあると、ビュー側の`@tasks.each do |task|`でkeyを指定して、特定のタスクの`avatars`（コメントしたユニーク化されたユーザーの配列）を取得できる
```
    @tasks.each do |task|
      @users_with_avatars[task.id] = avatars(task)
    end
  end
```
- ハッシュ`@users_with_avatars`にtaskのidをkeyとして`avatars`（コメントしたユニーク化されたユーザーの配列）を入れていく

### tasks/index.html.haml
```
.card_avatars
```
- CSS適用のためのdiv
- CSSはシンプルなので今回は記載しません

```
  - @users_with_avatars[task.id].each do |avatar|
```
- 裏側で作成した`@users_with_avatars`にキーを渡して、配列`avatars`を取得
- user.idが詰まった配列`avatars`なので、user.idを一つ一つ取り出し以下に`avatar`として活用

```
    - if User.find(avatar).avatar.attached?
```
- Userモデルから、idをもとにインスタンスを取得
- 取得したインスタンスにavatarがあるかどうかを判定
    - アバターの設定は必須としていないため
```
      = image_tag task.user.avatar, class: 'card_avatar'
    - else
      = image_tag 'default-avatar.png', class: 'card_avatar'
```
- アバターが設定されているユーザーならアバターを表示
- そうでないならデフォルトのアバターを表示
- これを`avatar`の要素分繰り返す
- 表示されたアバターをCSSで横並びにしてネガティブマージンを使って重ねる

## 学びになった点
- includeメソッドと!でユニーク化ができる
- @tasksのような配列を使わないといけない状況下の場合、ハッシュを使ってidをキーにして設定するとうまくいく