[Rails] プロフィール機能を実装する

学習記録

はじめに

プロフィール編集機能を実装するやり方をみていきましょう。
ユーザのプロフィール画面のURLは、idを含む必要がありません。ユーザーに対するプロフィールは1つしか存在しないからです。また、他のユーザのプロフィールを編集することも想定されない為、そもそもidを含む必要がないのです。

ルーティング設定

今回は一覧画面(index.html.erb)が必要なく、詳細画面(show.html.erb)へのURLがidを含む形(profiles/:id)ですと自分が何番目に作成されたユーザか外部から分かってしまいますし、詳細画面へのURLのidの部分を他のユーザのidに書き換えられる危険性もあります。
ですから、単数形リソースでルーティングを設定しましょう。

resource :profile, only: %i[show edit update]
edit_profile GET    /profile/edit(.:format)             profiles#edit
     profile GET    /profile(.:format)                  profiles#show
             PATCH  /profile(.:format)                  profiles#update
             PUT    /profile(.:format)                  profiles#update

CarrierWaveを使ってアップローダークラスを作成

$ bundle exec rails g uploader Avatar

コマンドを実行すると以下のapp/uploaders/board_image_uploader.rbが作成されます。

生成されたアップローダにデフォルト画像ファイルとアップロード可能な拡張子の設定を追記します。

class AvatarUploader < CarrierWave::Uploader::Base
  storage :file

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  def default_url
    'sample.jpg'
  end

  def extension_whitelist # 拡張子の制限
    %w[jpg jpeg gif png]
  end
end

上記のコードより、デフォルトでstorage :file が指定されているので、アップロードしたファイルはpublic/ 配下に保存されます。

default_url は画像を添付しなかった場合に、自動で登録されるわけではありません。@user.avatar.nilなどで呼び出してnilだった場合に、代わりに呼び出されるURLとなります。保存をしていないことでdefault_urlの値を書き換えるだけで画像が更新されます。

アバター画像のカラムを追加

usersテーブルにavatarカラムを追加します。

$ rails g migration add_avatar_to_users avatar:string

作成されたマイグレーションを確認しましょう

class AddAvatarToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :avatar, :string
  end
end

マイグレーションします。

$ rails db:migrate

なお,DBに保存されるのは「画像データ」ではなく「画像のファイル」であることに注意しましょう。

アップローダークラスとカラムの紐付け

avatar カラム」と「AvatarUploader クラス」を紐付けます。

mount_uploader :avatar, AvatarUploader

コントローラの設定

新たにprofiles_controllerを作成しましょう。

$ bundle exec rails g controller profiles

今回作成するprofilesコントローラはモデルと紐付きません。
何故わざわざprofilesコントローラを作成して、ユーザの編集画面へ遷移するような設計にするのでしょうか?Usersコントローラからedit アクションを用意して /users/:id/edit のように遷移することも考えられます。
しかしこれだとURLに不要なidが含まれてしまいます。ユーザの編集画面は自分自身のプロフィールにだけ存在すれば事足りるのです。「はじめに」でも言及しましたが、他のユーザのプロフィールを編集することは想定されていませんので、profile というresource (単数形リソース)でルーティング設定をし、:id の含まないURLを生成しているのです。

class ProfilesController < ApplicationController
  before_action :set_user, only: %i[edit update]

  def show;end

  def edit;end

  def update
    if @user.update(user_params)
      redirect_to profile_path, success: t('defaults.message.updated', item: User.model_name.human)
    else
      flash.now['danger'] = t('defaults.message.not_updated', item: User.model_name.human)
      render :edit
    end
  end

  private

  def set_user
    @user = User.find(current_user.id)
  end

  def user_params
    params.require(:user).permit(:email, :first_name, :last_name, :avatar, :avatar_cache)
  end
end

画像ファイルをコントローラで受けとるように忘れずストロングパラメータに追記しましょう。

コードを紐解く

def set_user
  @user = User.find(current_user.id)
end

プロフィール編集対象のユーザ情報(@user)をcurrent_userのインスタンスではなく、DBから取得したオブジェクトを利用しましょう。

# BAD
def set_user
  @user = current_user
end

# GOOD
def set_user
  @user = User.find(current_user.id)
end

BAD例で実装すると、プロフィール名を変更失敗した際、画面上で名前が変わってしまいます。これはプロフィール編集失敗時にcurrent_userupdateの影響を受けてしまう為です。

ビューファイルの作成

プロフィール詳細画面

<% content_for(:title, t('.title')) %>
<div class="container pt-3">
  <div class="row">
    <div class="col-md-10 offset-md-1">
      <h1 class="float-left mb-5"><%= t('.title') %></h1>
      <%= link_to t('defaults.edit'), edit_profile_path, class: 'btn btn-success float-right' %>
      <table class="table">
        <tr>
          <th scope="row"><%= User.human_attribute_name(:email) %></th>
          <td><%= current_user.email %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:full_name) %></th>
          <td><%= current_user.decorate.full_name %></td>
        </tr>
        <tr>
          <th scope="row"><%= User.human_attribute_name(:avatar) %></th>
          <td><%= image_tag current_user.avatar.url, class: 'rounded-circle', size: '50x50' %></td>
        </tr>
      </table>
    </div>
  </div>
</div>

プロフィール編集画面

<% content_for(:title, t('.title')) %>
<div class="container">
  <div class="row">
    <div class="col-md-10 offset-md-1">
      <h1><%= t('.title') %></h1>
      <%= form_with model: @user, url: profile_path, local: true do |f| %>
        <%= render "shared/error_messages", object: f.object %>
          <div class="form-group">
            <%= f.label :email %>
            <%= f.email_field :email, class: "form-control mb-3" %>
          </div>
          <div class="form-group">
            <%= f.label :last_name %>
            <%= f.text_field :last_name, class: "form-control mb-3" %>
          </div>
          <div class="form-group">
            <%= f.label :first_name %>
            <%= f.text_field :first_name, class: "form-control mb-3" %>
          </div>
          <div class="form-group">
            <%= f.label :avatar %>
            <%= f.file_field :avatar, class: "form-control", accept: 'image/*', onchange: 'previewImage(preview)' %>
            <%= f.hidden_field :avatar_cache %>
            <div class='mt-3 mb-3'>
              <%= image_tag @user.avatar.url, id: 'preview', size: '100x100', class: 'rounded-circle' %>
            </div>
          </div>
          <%= f.submit  class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
</div>

コードを紐解く

以下は画像ファイルの入力フィールドですが、プレビュー機能を実装するための記述を加えてあります

 <%= f.label :avatar %>
 <%= f.file_field :avatar, class: "form-control", accept: 'image/*', onchange: 'previewImage(preview)' %>
 <%= f.hidden_field :avatar_cache %>
 <div class='mt-3 mb-3'>
   <%= image_tag @user.avatar.url, id: 'preview', size: '100x100', class: 'rounded-circle' %>
イベントハンドラ説明
onchangeフォーム要素の選択、入力内容が変更された時に発生

image_tag の部分でアップロードした画像を表示しています。
image_tagのidとJavaScriptのファイルを紐つけています。

function previewImage() {
    const target = this.event.target;
    const file = target.files[0];
    const reader  = new FileReader();
    reader.onloadend = function () {
        const preview = document.querySelector("#preview")
        if(preview) {
            preview.src = reader.result;
        }
    }
    if (file) {
        reader.readAsDataURL(file);
    }
  }

ヘッダーのパーシャルファイル

ヘッダーからプロフィール詳細画面へ遷移するリンクとアバター画像の表示も修正しておきましょう

<li class='nav-item dropdown dropdown-slide'>
  <a href='#' class='nav-link' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false' id='header-profile'>
    <%= image_tag current_user.avatar_url, class: 'rounded-circle mr15', size: '40x40'%>
  </a>
  <div class='dropdown-menu dropdown-menu-right'>
    <div class='dropdown-item'><%= current_user.decorate.full_name %></div>
    <div class='dropdown-divider'></div>
    <%= link_to t('profiles.show.title'), profile_path, class: 'dropdown-item' %>
    <%= link_to t('defaults.logout'), logout_path, class: 'dropdown-item', method: :delete %>
  </div>
</li>

終わりに

プレビュー機能はJavaScriptを使っているので難しいですが、導入するとかっこよくなりますね!

参考記事

Rails のルーティング - Railsガイド
Railsのルーティング機能について解説します。Railsアプリケーションで行われているルーティングのしくみについて理解したい場合は、ここからお読みください。
単数形リソース
値渡しと参照渡しの違いを理解する
【Ruby】メソッドの引数は値渡し?参照渡し? - Qiita
①〜④でreplaceメソッド実行後の変数aの値はそれぞれ何になるでしょうか。この記事ではメソッドの引数と、メソッド呼び出し元の変数について動きを追って見ていきます。①破壊的メソッドによる変更a…
プロフィール編集機能の実装 - Qiita
備忘録です!!##新たなカラムの追加userのアバター画像のカラムを生成します。rails g migration add_avatar_to_users avatar:string##vi…

コメント

タイトルとURLをコピーしました