【Rails】carrierwaveで実装した画像アップロード機能にプレビューやアイコンを加えてお洒落に改造する

## 概要

![スクリーンショット 2021-02-21 23.40.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/30119467-e965-87f0-b507-63d1a4ef6ddc.png)

Ruby on Railsで作ったWebアプリに画像アップロード機能を実装したい場合、特に有力な選択肢として挙がる「[carrierwave](https://github.com/carrierwaveuploader/carrierwave)」ですが、よくあるチュートリアル通りにやっただけでは↑みたいな感じでやや素っ気ないデザインになってしまいがち...。

そこで今回は、プレビューを付けたり綺麗なアイコンを追加したりして少しだけお洒落に改造してみます。

## 完成イメージ

![マイ-ムービー.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/ef1bcbd1-1105-51fb-b9a4-7eb11a03dbac.gif)

プレビューできるようになるだけで一気に使用感が良くなりますね。

## 環境

- 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
```

```dockerfile:./Dockerfile
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"]
```
```yml:./docker-compose.yml
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:
```
```ruby:./Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 5.2.4'
```
```:./Gemfile.lock
空欄でOK
```
```sh:./entrypoint.sh
#!/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を編集

```yml:./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にアクセス

![スクリーンショット 2021-02-22 0.04.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/ac3a6b7a-03b0-f065-2992-e4acd833bf90.png)

おなじみの画面が表示されれば環境構築は完了です。

### 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 にアクセスして

![スクリーンショット 2021-02-22 0.08.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2604b221-439d-565a-fd49-cb4e7efb69f1.png)

こんな感じになってればOKです。

### carrierwaveを導入

今のままだと画像をアップロードする機能までは実装できていないため、carrierwaveを導入していきます。

```ruby:./Gemfile
...

gem 'carrierwave'

...
```

Gemfileが更新されたので再ビルド。

```
$ docker-compose build
```

次のコマンドでuploaderクラスを作成します。

```
$ docker-compose run web rails g uploader Image
```

```ruby:./app/uploaders/image_uploader.rb
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
```

こんな感じのファイルが自動で作成されているはず。お好みに応じて色々いじれる部分もありますが、今回は一旦デフォルトの状態で進めます。

```ruby:./app/models/post.rb
class Post < ApplicationRecord
  mount_uploader :image, ImageUploader
end
```

postモデルとuploaderを紐付け。

```erb:./app/views/posts/_form.html.erb
...

# 変更前
<div class="field">
  <%= form.label :image %>
  <%= form.text_field :image %>
</div>

...

# 変更後
<div class="field">
  <%= form.label :image %>
  <%= form.file_field :image %>
</div>

...
```

```erb:./app/views/posts/show.html.erb
...

# 変更前
<p>
  <strong>Image:</strong>
  <%= @post.image %>
</p>

...

# 変更後
<p>
  <strong>Image:</strong>
  <%= image_tag @post.image.url %>
</p>

...
```

上記2ファイルを変更し、

- http://localhost:3000/posts/new
- http://localhost:3000/posts/1

にアクセスして画像アップロードおよび画像表示ができるようになっていれば成功です。


![スクリーンショット 2021-02-22 0.21.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/0ea76c67-7284-2a36-758e-7b12e5837b37.png)

![スクリーンショット 2021-02-22 0.22.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/033af31c-3288-fdf0-7f06-04b093da910b.png)

もし次のようなエラー画面が表示されてしまった場合、コンテナを再起動させてください。

![スクリーンショット 2021-02-21 21.29.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/258eef52-c456-0ff1-00ba-44e009daaba8.png)

```
$ docker-compose restart
```

### プレビューやアイコンで装飾

いよいよここから本格的な装飾に入ります。

```ruby:./Gemfile
...

gem 'jquery-rails'
gem 'font-awesome-sass'
gem 'simple_form'

...
```
```
$ docker-compose build
```

Gemfileに追記し再ビルド。

```css:./app/assets/stylesheets/appclication.css→./app/assets/stylesheets/appclication.scss
...

@import 'font-awesome-sprockets';
@import 'font-awesome';

...
```

「./app/assets/stylesheets/appclication.css」→「./app/assets/stylesheets/appclication.scss」にリネームし、font-awsomeをインポートします。

```js:./app/assets/javascripts/application.js
//= 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](https://github.com/heartcombo/simple_form) はフォームに関する記述をめちゃくちゃシンプルにしてくれるgemです。

強制ではありませんが、僕はコイツが好きなので個人開発では多用しており、この記事でもそれ前提でコードを実装していきます。

何が良いのかというと、たとえばRails標準のform_withなどを使う場合、

```erb:
<%= 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を使うと

```erb:
<%= 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
```
```js:./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);
  });
});
```

```css:./app/assets/stylesheets/post.scss
#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;
  }
}
```
```erb:./app/views/posts/_form.html.erb
<%= 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で見た目を整えていきます。

![スクリーンショット 2021-02-22 1.08.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/c1e5749f-2a2f-7c93-e05c-05d5512187de.png)

色々設定ファイルをいじったので再度「docker-compose restart」などでコンテナを再起動し、http://localhost:3000/posts/new にアクセスしてこんな感じになってれば成功です。

## あとがき

お疲れ様でした。simple_formを使った分、慣れていない人にとっては少し癖のあるコードになってしまったかもしれませんが、プレビューを表示するjavascriptなどはそのまま使えるはずなのでご自身のプロジェクトに合わせて色々いじってみてください。