Markdown editor preview modal

After adding Kramdown markdown to convert my blog articles markdown to html in when serving it up to readers, I decided it would also be useful to have a preview of some kind in the editor. I considered implementing something real-time, with rendering on one side in an iframe, and that may be something for ‘down the road. For now, this is what I had in mind:

preview button

modal preview image

Basically, the idea is to have a preview link formatted as a button on the toolbar, that makes an ajax post to the preview controller to create a preview, passing the current contents of the title and body fields in the article editing form. A partial containing my modal markup and the necessary erb to call my kramdown helper to return the article markup as HTML, an escape it properly, so it will render properly in the modal overlay.

Step 1: Adding the preview button to the markdown toolbar

app/views/articles/_toolbar.html.erb
1
2
 <!-- other buttons ommited for brevity -->
<a href="#" class="btn btn-default" id="show_preview" title="Preview Content" data-toggle="modal" data-target="#previewModal">Preview</a>

Step 2: The Ajax POST request

Since Rails plays nice with Coffeescript, let’s use it.

app/assets/javascripts/custom.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ready = ->
  $('#show_preview').bind 'click', ->
    title = $('input#article_title').val()
    body = $('textarea#article_body').val()
    $.ajax({
      type: "POST",
      url: "/previews/create",
      data: { preview: { title: title, body: body } },
      success:(data) ->
        return false
      error:(data) ->
        return false
    })
    return false
$(document).on 'ready page:load', ready

This is a little different than I may previously have set this up (pre-Rails 4). Because of Turbolinks, now stock in Rails, if I need to bind an action to a trigger in on my page, I’ll need to do so on ‘ready’ or ‘page:load.’ I do this by naming my function (loading it into a variable), then after the function, calling the variable to return the function on document ‘ready page:load’ (ready or page:load). It seems appropriate to name the function “ready”: ready = ->.

Step 3: Generate Previews controller and routes

The ready function binds a click action (listens for clicks) on the preview button (identified by id), and on click reads the current title and text fields in the article form. It then makes an Ajax POST to the create action in the Previews controller, which would be effective, if they existed.

I’ll generate them:

$ rails generate controller Previews create

Generating the controller and adding ‘create’ at the end gives me a Previews controller with an empty create method, and also updates the routes appropriately. Now I need to add a responds_to block into my create function, so it can respond by rendering the necessary javascript partial.

app/controllers/previews_controller.rb
1
2
3
4
5
6
7
class PreviewsController < ApplicationController
  def create
    respond_to do |format|
      format.js
    end
  end
end

Now, let’s check the routes file good measure.

config/routes.rb
1
2
3
4
5
Rails.application.routes.draw do
  post 'previews/create'
  .
  .  # routes removed for brevity
end

Step 4: The views

So far, so good, so I go ahead and create a javascript erb that will update my DOM appropriately with my preview partial.

app/views/previews/create.js.erb
1
2
html = "<%= escape_javascript(render 'preview') %>"
$(html).modal('show');

I have to use the escape_javascript method here on my rendered partial, to escape some HTML and new line /n characters, so it will be accepted as a valid javascript string.

Now, for the ‘preview’ partial the above javascript needs.

app/views/previews/_preview.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="modal fade" id="previewModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
        <h1 class="modal-title" id="myModalLabel"><%= escape_javascript(params[:preview][:title]) %>
        </h1>
      </div>
      <div id="article" class="modal-body">
        <%= kramdown params[:preview][:body] %>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close
        </button>
      </div>
    </div>
  </div>
</div>

I might as well style the modal a little.

app/assets/stylesheets/customer.css.scss
1
2
3
4
5
6
7
8
9
10
11
12
#previewModal {
  display:none;
  .modal-dialog{
    width: 768px;
  }
  .modal-title{
    border-bottom: 1px solid $gray-medium-light;
  }
  .modal-body{
    padding-top: 0px;
  }
}

That about does it. I can now use my nifty Markdown Toolbar to compose posts with a combination of Markdown, and HTML… Kramdown will also support Textile, as a nice bonus.

Kramdown

All someone needs to use to use Kramdown to render markup as HTML in Rails views is the Kramdown gem and a helper function like this one, which will take any markdown you send it, and return HTML.

app/helpers/application_helper.rb
1
2
3
4
5
module ApplicationHelper
  def kramdown(text)
     return sanitize Kramdown::Document.new(text, { coderay_line_numbers: :table }).to_html
  end
end

for example, I implement kramdown on article bodies in my views with this erb:

<%= kramdown @article.body %> .

I hope this was helpful as an exercise in using a custom Ajax call to preview unsaved form data.

Cheers!

Created 3/15/2015 11:11PM (MDT) | Last Updated 3/18/2015 12:11AM (MDT)

Comments

Log in to add comments.