はじめに
プロフィール編集機能を実装するやり方をみていきましょう。
ユーザのプロフィール画面の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_user
がupdate
の影響を受けてしまう為です。
ビューファイルの作成
プロフィール詳細画面
<% 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を使っているので難しいですが、導入するとかっこよくなりますね!
参考記事



コメント