Railsにmedium-editorでMedium.comライクな投稿機能を導入する

## はじめに

Ruby on Railsで[MediumEditor](https://yabwe.github.io/medium-editor/)を導入するまで。
[Medium.com](https://medium.com/)のかっこいいエディタをRailsプロジェクトに簡単に導入出来るので、オススメのWysiwygエディタです。

## 環境

* Ruby 2.4.0
* Rails 5.2.0

## 1. Railsプロジェクトの作成

Railsプロジェクトの作成からやっていきます。

```
$ rails new medium-editor
```

以上で`medium-editor`というプロジェクトを作成します。

## 2. gem install

使用するgemをインストールします。
`Gemfile`を開き以下を入力します。

```rb
# medium editor
gem 'medium-editor-rails'
gem 'medium-editor-insert-plugin-rails'
```

以上を記載したら、`bundle install`を行います。
gemをインストールしたら、medium-editorで使用するjsとcssを読み込ませます。

`app/assets/javascripts/application.js`に以下を追記

```js
//= require medium-editor
//= require medium-editor-insert-plugin
```

`app/assets/stylesheets/application.css`に以下を追記

```css
*= require medium-editor/medium-editor
*= require medium-editor/themes/beagle
*= require medium-editor-insert-plugin
```

## 3. scaffoldで投稿機能の作成

`scaffold`でCRUDの投稿機能をささっと作ってしまいます。

```
$ rails g scaffold Article title:string body:text

$ rails db:migrate
```

ここまで実行したら、[http://localhost:3000/articles](http://localhost:3000/articles)を確認して見ましょう。

## 4. routes.rbを変更する

[http://localhost:3000/articles](http://localhost:3000/articles)でもいいのですが、articlesと入力しなくてもアクセスが出来るようにしておきます。
`config/routes.rb`に以下のようにします。

```rb
Rails.application.routes.draw do
  resources :articles

  root 'articles#index'
end
```

## 5. フォームにmedium-editorを適用させる

`app/views/articles/_form.html.erb`を開く

テキストエリアに`editable`というクラスを付与させておく。
該当ファイルの最終行に以下を追記することで、medium-editorを適用することが出来る。

```html
<script>
  var editor = new MediumEditor('.editable', {
    // placeholder settings
    placeholder: {
      text: 'Type your story',
      hideOnClick: true
    }
  });
  
  $('.editable').mediumInsert({
    editor: editor
  });
</script>
```

## 6. 見た目を整える

`scaffold`をしただけでは、寂しいので、[Siimple](https://siimple.juanes.xyz/)を使って見た目を最低限整えてあげましょう。

### layouts/application.html.erb

`app/views/layouts/application.rb`を以下のようにします。
今回はCDN経由でSiimpleを使えるようにしておきます。

```erb
<!DOCTYPE html>
<html>
  <head>
    <title>MediumEditor</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/siimple@3.0.0/dist/siimple.min.css">
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
```

### articles/index.html.erb

`app/views/articles/index.html.erb`

```erb
<div class="siimple-content">
  <p id="notice"><%= notice %></p>

  <h1 class="siimple-h2">Articles</h1>

  <table class="siimple-box">
    <thead>
      <tr>
        <th>Title</th>
        <th>Body</th>
        <th colspan="3"></th>
      </tr>
    </thead>

    <tbody>
      <% @articles.each do |article| %>
        <tr>
          <td><%= article.title %></td>
          <td><%= link_to 'Show', article, class: 'siimple-link' %></td>
          <td><%= link_to 'Edit', edit_article_path(article), class: "siimple-link" %></td>
          <td><%= link_to 'Destroy', article, class: 'siimple-link', method: :delete, data: { confirm: 'Are you sure?' } %></td>
        </tr>
      <% end %>
    </tbody>
  </table>

  <br>

  <%= link_to 'New Article', new_article_path, class: 'siimple-link' %>
</div>
```

### articles/show.html.erb

`app/views/articles/show.html.erb`

```erb
<div class="siimple-size siimple-size--large">
  <div class="siimple-box">
    <p id="notice"><%= notice %></p>

    <p class="siimple-box-title">
      <strong>Title:</strong>
      <%= @article.title %>
    </p>

    <p class="siimple-box-detail">
      <strong>Body:</strong>
      <%= @article.body.html_safe %>
    </p>
  </div>
  <%= link_to 'Edit', edit_article_path(@article), class: 'siimple-link' %> |
  <%= link_to 'Back', articles_path, class: 'siimple-link' %>
</div>
```

### articles/_form.html.erb

`app/views/articles/_form.html.erb`

```erb
<%= form_with(model: article, local: true, class: 'siimple-form') do |form| %>
  <% if article.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:</h2>

      <ul>
      <% article.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="siimple-form-field">
    <%= form.label :title, class: 'siimple-label' %>
    <%= form.text_field :title, id: :article_title, class: 'siimple-input' %>
  </div>

  <div class="siimple-form-field">
    <%= form.label :body, class: 'siimple-label' %>
    <%= form.text_area :body, id: :article_body, class: 'editable siimple-textarea siimple-textarea--fluid' %>
  </div>

  <div class="siimple-form-field">
    <%= form.submit class: 'siimple-btn siimple-btn--blue' %>
  </div>
<% end %>
```

### articles/new.html.erb

`app/views/articles/new.html.erb`

```erb
<h1 class="siimple-h2">New Article</h1>

<%= render 'form', article: @article %>

<%= link_to 'Back', articles_path, class: 'siimple-link' %>
```

### articles/edit.html.erb

`app/views/articles/edit.html.erb`

```erb
<h1 class="siimple-h2">Editing Article</h1>

<%= render 'form', article: @article %>

<%= link_to 'Show', @article, class: 'siimple-link' %> |
<%= link_to 'Back', articles_path, class: 'siimple-link' %>
```

## 7. carrierwaveを使って画像を保存出来るようにする

以上までで、投稿が出来るようになったのですが、文中に挿入した画像を保存出来るようにします。

### Gemfile

`Gemfile`を開き、以下を追加

```rb
gem 'carrierwave'
```

追加したら、`bundle install`

### uploaderを生成

`rails g uploader Image`

### 画像保存用のリソースの作成

`rails g scaffold Image file:string`

### imageモデルの修正

生成された`app/models/image.rb`を以下のように修正

```rb
class Image < ApplicationRecord
  mount_uploader :file, ImageUploader
end
```

### routes.rbにpostメソッドを加える

`config/routes.rb`を開き、以下を加える

```rb
post 'images/upload', to: 'images#upload'
```

### images_controller.rb

`app/controllers/images_controller.rb`を以下のように編集する。

> scaffoldで生成したアクションは全て消してしまって大丈夫です。

```rb
class ImagesController < ApplicationController
  def upload
    files = params.require(:files)

    @image = Image.new
    @image.file = files[0]
    respond_to do |format|
      if @image.save
        format.html { redirect_to @image, notice: 'Image was successfully created.' }
        format.json do
          render json: {
            files:
              [
                {
                  url: @image.file.metadata['url']+"?id=#{@image.file.model.id}",
                  thumbnail_url: @image.file.metadata['url']+"?id=#{@image.file.model.id}",
                  size: 0,
                  delete_url: "/images/delete",
                  delete_type: "DELETE"
                }
              ]
          }
        end
      else
        format.html { render :new }
        format.json { render json: @image.errors, status: :unprocessable_entity }
      end
    end
  end
end
```

### articles/_form.html.erbを修正する

`app/views/articles/_form.html.erb`の最終行に追加した`script`タグの中身を変更する。

```erb
<script>
  $(document).ready(function(){
    var editor = new MediumEditor('.editable', {
      // placeholder settings
      placeholder: {
        text: 'Type your story',
        hideOnClick: true
      }
    });

    $('.editable').mediumInsert({
      editor: editor,
      addons: {
        images: {
          fileUploadOptions: {
            url: '/images/upload',
            type: 'post',
            acceptFileTypes: /(.|\/)(gif|jpe?g|png)$/i
          }
        }
      }
    });
  });
</script>
```

## 8. 完成形のスクリーンショット

<img width="1311" alt="スクリーンショット 2018-05-05 23.50.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/19716/bdcab6a5-323b-e54c-be5f-0a23847e1748.png">

<img width="1312" alt="スクリーンショット 2018-05-05 23.51.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/19716/86e0db04-b98d-1895-3cdb-2a8388101a18.png">

<img width="1308" alt="スクリーンショット 2018-05-06 0.24.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/19716/6b2fa1b5-2a2f-2289-4619-d9087f1d0af0.png">

<img width="1308" alt="スクリーンショット 2018-05-06 0.25.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/19716/581bda5b-335a-4d3e-8137-168023504c0f.png">




上記で一通り使えるまでには出来たかと思います。
そのうち、画像のストレージをCloudinaryに置き換えて画像を保存する方法について書こうと思います。


何かあればコメントください。