Ruby
RubyOnRails

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

はじめに

Ruby on RailsでMediumEditorを導入するまで。
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を開き以下を入力します。

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

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

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

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

app/assets/stylesheets/application.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を確認して見ましょう。

4. routes.rbを変更する

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

Rails.application.routes.draw do
  resources :articles

  root 'articles#index'
end

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

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

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

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

  $('.editable').mediumInsert({
    editor: editor
  });
</script>

6. 見た目を整える

scaffoldをしただけでは、寂しいので、Siimpleを使って見た目を最低限整えてあげましょう。

layouts/application.html.erb

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

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

<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

<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

<%= 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

<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

<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を開き、以下を追加

gem 'carrierwave'

追加したら、bundle install

uploaderを生成

rails g uploader Image

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

rails g scaffold Image file:string

imageモデルの修正

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

class Image < ApplicationRecord
  mount_uploader :file, ImageUploader
end

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

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

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

images_controller.rb

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

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

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タグの中身を変更する。

<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. 完成形のスクリーンショット

スクリーンショット 2018-05-05 23.50.53.png

スクリーンショット 2018-05-05 23.51.06.png

スクリーンショット 2018-05-06 0.24.40.png

スクリーンショット 2018-05-06 0.25.40.png

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

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