RailsにMarkdown導入してプレビュー表示させるまで


はじめに

最近Markdown記法に触れ感銘を受けたプログラミング初心者です。
Rails始めてはや一ヶ月、Rails上でテキストエリアをMarkdown対応させるくらいできるでしょ?できらぁ!!ということで始めていきます。ちなみにマークアップ弱者なのでmaterializeを使っています。cssのclass関連のコードは適宜読み替えてください。

環境

  • Rails 5.2.3
  • Ruby 2.5.1

今回やること

  • Rails環境下でテキストエリアをMarkdown記法に対応させる。
  • jQueryでプレビュー機能を実装する。

利用するgem

Gemfile
gem 'redcarpet'
gem 'jquery-rails'

RedcarpetはMarkdownで書かれた文字列をhtmlに変換してくれるgemです。またjQueryは非同期なプレビュー機能実装のために利用します。$ bundle installしておきましょう。

Helperメソッドの作成

上記に述べたredcarpetの機能を利用するため、それ用のhelperを作っていきます。今回はapp/helpers/にmarkdown_helper.rbを作成します。

app/helpers/markdown_helper.rb
module MarkdownHelper
  def markdown(text)
    unless @markdown
      options = {
        hard_wrap: true
      }
      extensions = {
        no_intra_emphasis: true,
        tables: true,
        fenced_code_blocks: true,
        autolink: true,
        quote: true
      }
      renderer = Redcarpet::Render::HTML.new(options)
      @markdown = Redcarpet::Markdown.new(renderer, extensions)
    end

    @markdown.render(text).html_safe
  end
end

rendererのoptionとMarkdownのextensionsをそれぞれ設定します。住み分け基準がよくわからないので、わたしのような初心者はextensionの内容をoptionに渡したりしてハマります。
公式READMEを参照しながら設定願います。
ひとまずこれでヘルパーは完成です。viewファイルでヘルパーを呼び出します。

show.html.haml
-# 省略
%span
 = markdown(@article.body)

Viewの確認

Markdown導入に関しては以上です。実際のviewを確認してみます。

ちゃんとhtmlに変換してくれています。

プレビューの実装

最初に述べた通りjQueryでプレビューを実装するのでapplication.jsに以下の一文を追記します。

application.js
//= require jquery

上のgifにちらっと写っていますが、今回実装したい挙動は以下のとおりです。

  • previewボタンを押したらテキストエリアを消して、プレビューを表示
  • markdownボタンを押したらプレビューを消して、テキストエリアに戻る

では作業を進めていきます。

ajax用のルーティングを作成

既存のコントローラーに書いてしまってもよいのですが、今回はajax用のコントローラーを別に用意します。
まずはroutes.rbにルーティングを追加します。

app/config/routes.rb
# 省略
  namespace :api, format: 'json' do
    get 'articles/preview'
  end

"api"という名前空間を使用してjson形式のリクエストを受けるように指定します。

次にこの名前空間を利用したコントローラーを準備します。
app/controllers/apiディレクトリを作成し、そのなかにarticles_controller.rbを作成します。

articles_controller.rb
class Api::ArticlesController < ApplicationController
  def preview
    @html = view_context.markdown(params[:body])
  end
end

さっきつくったヘルパーを使い回しましょう。
コントローラーからヘルパーメソッドを呼ぶためにview_contextメソッドを使用します。

つぎに作成したhtmlをjsonに突っ込ませます。
app/views/api/articlesディレクトリを作成し、そのなかにpreview.json.jbuilderを作成します。

preview.json.jbuilder
json.body @html

これでajaxのための準備ができました。あとはjQueryのコードを記述するだけです!!

htmlタグにidを仕込む

その前にボタンやインプットまわりにidを仕込んでおきます。

new.html.haml
-# 省略
= form_for @article do |f|
 .row
  #preview-area 
   .input-field
     = f.label "content", for: "md-textarea"
     = f.text_area :body, class: "materialize-textarea", id: "md-textarea"
 .row
   .col.s4
     -# ページに遷移してきたタイミングでは押せないようにしておく
     .btn#markdown.disabled markdown
   .col.s4
     .btn#preview preview
   .col.s4
     = f.submit "send", class: "btn"

テキストエリアとマークダウン、プレビューボタンにidを仕込みました。またhtmlを突っ込むための#preview-areaを用意しておきます。
articles#newのページに遷移してきたタイミングではテキストエリアが表示されているので、マークダウンボタンは押せないようにしておきます。

jQuery

app/assets/javascripts以下にpreview.jsを作成します。
さきにコード全体をのっけておきます。

preview.js
$(function() {
  // previewボタンが押されたらイベント発火
  $('#preview').on('click', function() {
    var text = $('#md-textarea').val();
    if (text == "") {
      return;
    }
    $.ajax({  
      url: '/api/articles/preview',
      type: 'GET',
      dataType: 'json',
      data: { body: text }
    })
    .done(function(html) {
      // ajax成功したら、テキストエリアを非表示にする。
      $('#md-textarea').parent().css('display', 'none');
      $('#preview-area').append(html.body);
      // markdownボタンとpreviewボタンのdisabledを入れ替える。
      $('#markdown').removeClass('disabled');
      $('#preview').addClass('disabled');
    })
    .fail(function() {
      alert('failed for markdown preview');
    })
  })

  // markdownボタンが押されたらイベント発火
  $('#markdown').on('click', function() {
    $('#md-textarea').parent().css('display', '');
    $('#preview-area').empty();
    $('#preview').removeClass('disabled');
    $('#markdown').addClass('disabled');
  })
})

イベント発火のタイミングはpreviewボタン押下時とmarkdownボタン押下時の二つです。
previewボタン押下時にajaxを行い、帰ってきたhtmlを#preview-areaにappendします。あとは有効なボタンを入れ替えたり、テキストエリアを隠したりといった操作をしています。

以上で作業は終了です。

挙動を確認


ちゃんと動きましたね。よかったぁー。
おてもとの環境でも動いていますでしょうか?

さいごに

なんだか長くなってしまいました。初投稿ということでゆるしてください。
一応サンプルをgithubに置いときます。プログラミングについては初心者丸出しなので、コーディングのアドバイスあればお願いします!!

参考