ユーザー情報にアイコンとプロフィールを追加する方法を丁寧に解説（rails）

# はじめに

こんにちは。今回はSNSなどで普段よく目にするアイコンとプロフィールの実装方法を解説していきます。
LINE、Twitter、Instagram、YouTubeなどで目にするアイコンは全て丸い形をしているので、そのようなデザインにする方法も一緒に解説します。このCSSのレイアウト調整が意外と苦戦しました(汗)

それにしてもなんでアイコンって正方形じゃなくて丸いんだろう、とふと疑問に思ったので調べてみたのですが、どうやらTwitterなどにある添付画像が四角いのでパッと見でアイコンだと区別がつくように丸くしたようです。

余談はこの辺にして実際の完成イメージからご紹介します。

# 完成イメージ

今回は編集画面でアイコンを追加、追加したアイコンを詳細画面に表示のみご紹介しますが、
新規登録時にアイコンを追加できるようにしたり、投稿と紐付けてユーザー名の横にアイコンを表示させたりも
今回の実装をベースにすれば容易に実現可能だと思います。

![exhoop_200321_icon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487789/53f93c7f-7bcc-fd3b-361b-a0df976a1b02.gif)

容量の関係で画質荒めですが、イメージはこんな感じです。
編集画面では非同期で画像をプレビューし、Updateボタンで詳細画面に遷移させるといった流れになります。

それでは行きましょう！

# 環境

- ruby 2.5.1
- Rails 5.2.3
- mysql
- Haml 5.1.2
- Ruby Sass 3.7.4

# 前提条件
- deviseを用いてユーザーの新規登録・ログイン・ログアウト・編集・削除・詳細画面の実装済みであること
- Jqueryが使えるようにgemやapplication.jsに必要な記載を追加・実装済みであること
- carrierwave, minimagickをgemで導入済みであること

# 開発の流れ

1. Usersテーブルにアイコンとプロフィールのカラムを追加
2. Usersコントローラーにアイコンとプロフィールのカラムを追加
3. 画像アップロード用のファイルを作成
4. 編集画面のビューファイルにアイコンとプロフィールを追加
5. 編集画面のビューに対応するscssファイルでレイアウトを調整
6. 非同期で画像プレビューするためにJSファイルを作成
7. 詳細画面に表示する用のビュー、レイアウトを調整

# 手順

## 1. Usersテーブルにアイコンとプロフィールのカラムを追加

下記のようにターミナルからコマンドを打ちカラムを追加します。
アイコンはファイル名で保存されるので文字列のstring型、プロフィールは不定長文字列のtext型で定義します。

```ターミナル.
$ rails g migration AddImageAndProfileToUsers image:string profile:text
$ rails db:migrate
```


## 2. Usersコントローラーにアイコンとプロフィールのカラムを追加

user_paramsでimageカラムとprofileカラムをpermit内に追加して、データを更新できるようにしておきましょう。
showアクションでアイコンやプロフィールを表示するためにそれぞれ@image, @profileにデータを入れてあげます。
editアクションでも既にimageにデータが入っている場合に表示させるので@imageを追加してあげます。
ここでプロフィールやその他のデータがeditアクション内に無いのは後ほど手順5で解説します。


```users_controller.rb
class UsersController < ApplicationController

  def edit
    user = User.find(params[:id])
    @image = user.image
  end

  def update
    if current_user.update(user_params)
      redirect_to user_path(current_user)
    else
      render :edit
    end
  end

  def show
    user = User.find(params[:id])
    @id = user.id
    @name = user.name
    @image = user.image
    @profile = user.profile
    @videos = user.videos.order("created_at DESC")
  end

  private

  def user_params
    params.require(:user).permit(:id, :name, :email, :image, :profile)
  end
end
```

機能的に無くても動作には影響しませんが、ついでにストロングパラメーターにimageカラムを追加してあげましょう。
ストロングパラメーターは意図しない登録・更新を防ぐ、いわば脆弱性対策です。

```application_controller.rb
class ApplicationController < ActionController::Base
  (省略)
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :image]) 
  end
end
```


## 3. 画像アップロード用のファイルを作成

画像をアップロードするためにCarrierWave, MiniMagickの仕組みを使います。
ターミナルで下記を入力することで、uploadersフォルダ下にファイルが作成されます。

```ターミナル.
$ rails g uploader Image
```

作成されたファイルを開き、上から4行目あたりにある下記の1行がコメントアウトされているので外してください。

```uploaders/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base
  (省略)
  include CarrierWave::MiniMagick
  (省略)
end
```

ImageUploaderをuserモデルに追加することで先ほど作成したImageUploaderが適用されます。
追加で、これは好みですがプロフィール文の文字数を最大250文字に制限しています。

```model/user.rb
class User < ApplicationRecord
  (省略)
  validates :profile, length: { maximum: 250 }
  mount_uploader :image, ImageUploader
end
```


## 4. 編集画面のビューファイルにアイコンとプロフィールを追加

ここでかなり苦戦しました。
何故苦戦したかと言うと、アイコンの枠＞既に画像が存在する場合の表示＞新しく選択した画像＞ファイル選択用のボタンといった具合に、アイコンの部分だけでも４つの要素を用意しなければならず、その関係性を崩さないようにCSS、JQueryで調整しなければならなかったからです。他に簡略的に書ける良い方法がある場合は下のコメント欄にてご指摘ください。

```users/edit.html.haml
(省略)
    = form_for(current_user) do |f|
      //アイコン追加部分
      .account-page__inner--icon
        .account-page__inner--icon__label
        .account-page__inner--icon__input
          - if @image.present?
            = image_tag @image.url, class: 'account-page__inner--icon__input__image', width: '100%'
          .account-page__inner--icon__input__image2
          = f.file_field :image, class: 'account-page__inner--icon__input__btn', id: 'upload-icon'

      .account-page__inner--user-form
        (省略)
        // プロフィール追加部分
        .field
          .field-label
            = f.label :profile
          .field-input
            = f.text_area :profile, autocomplete: 'off'
        (省略)
```

### アイコンの解説
form_forを用いてfile_fieldでファイル選択用のフォームを用意します。
form_forについて忘れちゃった・詳しく知りたいという方はこちら（https://pikawaka.com/rails/form_for）

要素を整理するとこのようになります。
.account-page_\_inner--icon_\_input（アイコンの枠）
.account-page_\_inner--icon_\_input_\_image（既に画像が存在する場合の表示）
.account-page_\_inner--icon_\_input_\_image2（新しく選択した画像）
.account-page_\_inner--icon_\_input_\_btn（ファイル選択用のボタン）

手順2のUsersコントローラーのshowアクションでimageだけ@image=user.imageとしていた理由ですが、既に画像が存在する場合（if @image.present?）という条件をつけ、ある場合はその画像を表示する必要があるためです。プロフィールについてはtext_area部分でカラムに対して直接書き込むだけで、アイコンのような特殊な表示切り替えを行っていないため、わざわざ変数に置き換えてやる必要がありません。

画像の表示にはimage_tagを使用し、@image.urlで画像データを呼び出します。width: '100%'にすることでアイコンの枠（幅:180px, 高さ:180px）に対して幅いっぱいとなるので画像がぴったり真ん中に来るようになります。

JQueryで非同期で画像プレビューさせるのはファイル選択用のボタンを発火源とするので、
ここにid名：upload--iconをつけておきます。Jqueryの実装については手順6で後ほど解説します。

### プロフィールの解説
これは簡単ですね。
複数行のテキストを保存したいのでtext_areaを使用して、autocomplete: 'off'で入力候補の表示をオフにするだけです。
ついでにlabelで入力フォームの上にprofileという文字を表示しておきましょう。



## 5. 編集画面のビューに対応するscssファイルでレイアウトを調整

さて、いよいよレイアウトの調整に入っていくわけですが、非常に複雑ですのでポイント毎に順を追って解説していきます。

### アイコン枠を丸くする方法

幅と高さを同じサイズにし、border-radius: 100pxで要素の角が取れて丸くなります。
このinputは.account-page_\_inner--icon_\_input（アイコンの枠）です。

```user.scss
&__input {
  width: 180px;
  height: 180px;
  border-radius: 100px;
}
```

### 親要素からはみ出した部分を非表示にする

下記を追加しないとアイコンの枠は丸くしたものの、追加する画像が四角い場合、親要素の丸からはみ出て表示されてしまいます。

```user.scss
&__input {
  overflow: hidden;
}
```

### アイコン枠内に、既に画像が存在する場合の表示、新しく選択した画像、ファイル選択用のボタンを全て入れ込む

少し長いですが内容としては簡単です。親要素であるinputにposition: rerative、小要素全てにposition: absoluteを記述して、あとは配置を調整。btn部分は表示する必要がないのでopacity: 0で透明化し、cursor: pointerでマウスホバー時にカーソルマークが変わるようにしています。btnに対してopacity: 0では無くdisplay: noneするとボタンが無効化されてしまうので注意してください。

btn部分で幅と高さを1000pxにして親要素に対してright: 0;にしているのには理由があります。このやり方は正攻法ではないと思いますが、私なりに調べ模索した方法になります。このような記述にしないといけなくなった問題は下記の通りです。
file_fieldでファイル選択用のボタンが追加されますが、「ファイルを選択」という部分にはcursor: pointerが有効にならない。一方で「選択されていません」という文字部分にはcursor: pointerが適用されるといった謎な仕様になっています。そこでサイズをめちゃくちゃ大きくし親要素に対してright: 0とすることでカーソルが適用される「選択されていません」部分だけでアイコン枠を埋めてしまおうといった魂胆です。良い子はマネしないで下さい。
↓
2020/04/20
解決しました！解決方法は以下の通り。
・ 親要素と同じ幅、高さ180pxに変更
・ font-size: 0pxを追加し「ファイルを選択」部分をなくす
・ border-radiusでボタンを丸くする


```user.scss
&__input {
  margin: auto;
  width: 180px;
  height: 180px;
  border-radius: 100px;
  overflow: hidden;
  position: relative;
  &__image {
    position: absolute;
    top: 0;
    left: 0;
  }
  &__image2 {
    position: absolute;
    top: 0;
    left: 0;
  }
  &__btn {
    position: absolute;
    top: 0;
    right: 0;
    // 解決方法(追加)
    width: 180px;
    height: 180px;
    border-radius: 100px;
    font-size: 0px;
    // 解決方法(ここまで)
    opacity: 0;
    cursor: pointer;
  }
```

### プロフィールは大したことないですが、一応載せておきます

```user.scss
textarea {
  margin: auto;
  width: 400px;
  height: 150px;
  background-color: black;
  color: white;
  border: solid 1px rgb(150, 150, 150);
  border-radius: 5px;
  padding: 10px;
}
```


## 6. 非同期で画像プレビューするためにJSファイルを作成

ファイル名は○○.jsなら何でも良いです。
先ほど作成した編集画面のビューファイルの中でファイル選択用のボタンを発火源とするため、id名：upload-iconを追加しました。この発火源を\$fileField = $('#upload-icon')と書きます。画像を格納したい部分.account-page_\_inner--icon__input__image2を$previewに代入し、emptyで一旦空に、その後appendで画像を追加します。ここでimgタグを用いていますが、動画素材の場合はvideoタグを指定してやれば動画のプレビューも作ることができます。細かいところは私もまだまだ勉強中なので解説できませんが、流れとしてはこのような感じです。

```app/assets/javascripts/icon_preview.js
$(function(){
  $fileField = $('#upload-icon')
  
  $($fileField).on('change', $fileField, function(e) {
    file = e.target.files[0]
    reader = new FileReader(),
    $preview = $(".account-page__inner--icon__input__image2");

    reader.onload = (function(file) {
      return function(e) {
        $preview.empty();
        $preview.append($('<img>').attr({
          src: e.target.result,
          width: "100%",
          class: "preview",
          title: file.name
        }));
      };
    })(file);
    reader.readAsDataURL(file);
  });
});
```

## 7. 詳細画面に表示する用のビュー、レイアウトを調整

最後の工程です！ここまででアイコン・プロフィールの追加は完了しているので、追加したこれらをユーザー詳細画面に表示していきましょう。手順2でコントローラーは既に追記済みなのでビューファイルに書くだけです。

アイコンは編集画面のビューファイル同様に画像が存在する場合（-if @image.present?）という条件をつけてあげないと、画像がない状態で@imageを呼び出そうとしてエラーになります。

また、text_areaで追加したプロフィール文の改行を受付けるためにはsimple_formatをつけます。これを付けずに@profileだけ記載すると、編集画面で入力した際の改行が全て無効となってしまいます。

```users/show.html.haml
.content
  .userbox
    .userbox__top
      .userbox__top__left
        // アイコン表示部分
        .userbox__top__left__icon
          -if @image.present?
            = image_tag @image.url, width: '100%'
    (省略)
    // プロフィール表示部分
    .userbox__bottom
      = simple_format(@profile)
    (省略)
```

レイアウトは手順5を参考に実装してください。アイコン枠、現在の画像のみの表示なので難しくないですね。
これで完成イメージのようなアイコン・プロフィールが作れたかと思います！

### お疲れ様でした！

# 参考
https://qiita.com/shlia/items/d4fdd952b38d4140062d
https://qiita.com/akr03xxx/items/82ba45f7ef4fdbd5c702