概要
Ruby on Railsで作ったWebアプリに画像アップロード機能を実装したい場合、特に有力な選択肢として挙がる「carrierwave」ですが、よくあるチュートリアル通りにやっただけでは↑みたいな感じでやや素っ気ないデザインになってしまいがち...。
そこで今回は、プレビューを付けたり綺麗なアイコンを追加したりして少しだけお洒落に改造してみます。
完成イメージ
プレビューできるようになるだけで一気に使用感が良くなりますね。
環境
- Docker
- Ruby 2.5
- Rails 5.2.4
- MySQL 5.7
実装
誰でも同じ結果が得られるようにDockerfileを書くところから始めます。
ディレクトリを作成
$ mkdir carrierwave-lesson
$ carrierwave-lesson
各種ファイルを作成
$ touch Dockerfile
$ touch docker-compose.yml
$ touch Gemfile
$ touch Gemfile.lock
$ touch entrypoint.sh
FROM ruby:2.5.1
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
ENV APP_PATH /myapp
RUN mkdir $APP_PATH
WORKDIR $APP_PATH
COPY Gemfile $APP_PATH/Gemfile
COPY Gemfile.lock $APP_PATH/Gemfile.lock
RUN bundle install
COPY . $APP_PATH
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
version: '3'
services:
db:
image: mysql:5.7
command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_USER: root
MYSQL_PASSWORD: password
TZ: Asia/Tokyo
volumes:
- mysql-data:/var/lib/mysql
ports:
- 3306:3306
web:
build:
context: .
command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
volumes:
- .:/myapp
- ./vendor/bundle:/myapp/vendor/bundle
environment:
TZ: Asia/Tokyo
RAILS_ENV: development
ports:
- 3000:3000
depends_on:
- db
volumes:
mysql-data:
source 'https://rubygems.org'
gem 'rails', '~> 5.2.4'
空欄でOK
#!/bin/bash
set -e
# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
rails new
$ docker-compose run web rails new . --force --no-deps -d mysql
./config/database.ymlを編集
...
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>
username: root
password: password # デフォルトだと空欄になっているはずなので追記
host: db # デフォルトだと「localhost」になっているので書き換え
...
コンテナを起動
$ docker-compose build
$ docker-compose up -d
データベースを作成
docker-compose run web rails db:create
localhost:3000にアクセス
おなじみの画面が表示されれば環境構築は完了です。
Scaffoldでpost投稿機能を作成
今回は練習なのでScaffoldで一気に土台を作成してしまいます。
$ docker-compose run web rails g scaffold post content:text image:string
$ docker-compose run web rails db:migrate
http://localhost:3000/posts にアクセスして
こんな感じになってればOKです。
carrierwaveを導入
今のままだと画像をアップロードする機能までは実装できていないため、carrierwaveを導入していきます。
...
gem 'carrierwave'
...
Gemfileが更新されたので再ビルド。
$ docker-compose build
次のコマンドでuploaderクラスを作成します。
$ docker-compose run web rails g uploader Image
class ImageUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
# include CarrierWave::MiniMagick
# Choose what kind of storage to use for this uploader:
storage :file
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# Provide a default URL as a default if there hasn't been a file uploaded:
# def default_url(*args)
# # For Rails 3.1+ asset pipeline compatibility:
# # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
#
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
# end
# Process files as they are uploaded:
# process scale: [200, 300]
#
# def scale(width, height)
# # do something
# end
# Create different versions of your uploaded files:
# version :thumb do
# process resize_to_fit: [50, 50]
# end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
# def extension_whitelist
# %w(jpg jpeg gif png)
# end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
# def filename
# "something.jpg" if original_filename
# end
end
こんな感じのファイルが自動で作成されているはず。お好みに応じて色々いじれる部分もありますが、今回は一旦デフォルトの状態で進めます。
class Post < ApplicationRecord
mount_uploader :image, ImageUploader
end
postモデルとuploaderを紐付け。
...
# 変更前
<div class="field">
<%= form.label :image %>
<%= form.text_field :image %>
</div>
...
# 変更後
<div class="field">
<%= form.label :image %>
<%= form.file_field :image %>
</div>
...
...
# 変更前
<p>
<strong>Image:</strong>
<%= @post.image %>
</p>
...
# 変更後
<p>
<strong>Image:</strong>
<%= image_tag @post.image.url %>
</p>
...
上記2ファイルを変更し、
にアクセスして画像アップロードおよび画像表示ができるようになっていれば成功です。
もし次のようなエラー画面が表示されてしまった場合、コンテナを再起動させてください。
$ docker-compose restart
プレビューやアイコンで装飾
いよいよここから本格的な装飾に入ります。
...
gem 'jquery-rails'
gem 'font-awesome-sass'
gem 'simple_form'
...
$ docker-compose build
Gemfileに追記し再ビルド。
...
@import 'font-awesome-sprockets';
@import 'font-awesome';
...
「./app/assets/stylesheets/appclication.css」→「./app/assets/stylesheets/appclication.scss」にリネームし、font-awsomeをインポートします。
//= require jquery
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require image-preview
//= require_tree .
「image-preview」は後ほど作成するjsファイルですが、面倒なので先に読み込んでおきます。
$ docker-compose run web rails g simple_form:install
simple_form はフォームに関する記述をめちゃくちゃシンプルにしてくれるgemです。
強制ではありませんが、僕はコイツが好きなので個人開発では多用しており、この記事でもそれ前提でコードを実装していきます。
何が良いのかというと、たとえばRails標準のform_withなどを使う場合、
<%= form_with(model: post, local: true) do |form| %>
<%= form.label :title %>
<%= form.text_field :title %>
<%= form.label :body %>
<%= form.text_area :body %>
<% end %>
みたいな感じで
- string型なら「text_field」
- text型なら「text_area」
といった具合に書き分けなければなりませんが、simple_formを使うと
<%= simple_form_for(model: post, local: true) do |form| %>
<%= form.input :title, label: "title" %>
<%= form.input :body, label: "body" %>
<% end %>
たったこれだけで済みます。データの型を勝手に判別してくれるので、「input」と書くだけでそれぞれ自動で「text_field」と「text_area」として扱われるようになるんですね。
慣れるとこれ以外使いたくなるくらい便利なので、これを機に使ってみてください。
$ touch touch app/assets/javascripts/image-preview.js
$(function(){
$fileField = $('#file-input')
$($fileField).on('change', $fileField, function(e) {
file = e.target.files[0]
reader = new FileReader(),
$preview = $('#img_field');
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);
});
});
#img_field {
width: 306px;
padding: 5px 8px;
border: solid 1px #DEE2E6;
text-align: center;
position: relative;
padding: 0;
border-radius: 5px;
cursor: pointer;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
background-color: #fff;
overflow: hidden;
box-sizing: border-box;
transition: 0.3s ease-out;
&:hover {
background-color: #E8ECF1;
transition: 0.3s ease-out;
opacity: 0.9;
}
i {
font-size: 30px;
color: #aaa;
line-height: 150px;
transition: 0.3s ease-out;
}
}
<%= simple_form_for(post, url: posts_path, method: :post, local: true) do |form| %>
<% if post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% post.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.input :content, label: "content", input_html: { style: "width: 300px;" } %>
</div>
<div class="field">
<div id="img_field" onClick="$('#file-input').click()">
<% if post.image.present? %>
<%= image_tag(post.image.url) %>
<% else %>
<i class="fas fa-images"></i>
<% end %>
</div>
<%= form.input :image, as: :file, label: false, input_html: { style: "display: none;", id: "file-input" } %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
_form.html.erbをsimple_form版に一新し、cssとjavascriptで見た目を整えていきます。
色々設定ファイルをいじったので再度「docker-compose restart」などでコンテナを再起動し、http://localhost:3000/posts/new にアクセスしてこんな感じになってれば成功です。
あとがき
お疲れ様でした。simple_formを使った分、慣れていない人にとっては少し癖のあるコードになってしまったかもしれませんが、プレビューを表示するjavascriptなどはそのまま使えるはずなのでご自身のプロジェクトに合わせて色々いじってみてください。