Lugo Labs

Rewrite Medium in Ruby on Rails, Part 2: Creating the posts

In the first part of this short series on how to build a Medium site in Ruby on Rails, we setup the blog on an existing Rails app. We added the Post model and controller and showed a list of drafts and published posts. In this Part 2, we'll create and edit posts, using the Medium Editor library we installed on Part 1.

Creating the post

When users click on Write a post we want to take them to the form. When they start typing, we'll create a post and from then our new form becomes the edit form. We'll need to change the URL in the browser to point at the newly-created post.

Our edit.html.erb view looks like this:

html
<%= form_for [:blogr, @post], remote: true, html: { id: 'blogr-posts-form', class: 'blogr-form' } do |f| %>
  <%= f.text_area :title, class: 'editable title', id: 'blogr-editable-title', placeholder: 'Title' %>
  <%= f.text_area :body, class: 'editable body', id: 'blogr-editable-body', placeholder: 'Tell your story ...' %>
<% end %>

Notice how we use add the editable class to our elements, to help us with the Medium Editor. The form is going to be posted remotely, and auto-saved every second. Let's create a small object in CoffeeScript to help us with this:

js
# app/assets/javascripts/blogr.coffee

blogr =
  start: ->
    @_container = $("#posts-cont")
    self = this
    @_document = $(document)
    @_document.on 'ajax:success', (e, data) ->
      self._onLinkSuccess(e.target, data) if e.target.tagName == 'A'
      self._onFormSuccess(e.target, data) if e.target.tagName == 'FORM'
    @_document.on 'keyup', '#blogr-posts-form .editable', => @_create()
    @_loadMedium()

Since the Write new post link was a remote link, we'll need to manipulate the response by handling the ajax:success event triggered by Rails unobtrusive JavaScript (normally we should also handle ajax:error event, removed here for brevity).

js
# app/assets/javascripts/blogr.coffee

_onLinkSuccess: (link, data) ->
  history.pushState {}, null, link.href.replace('.html', '')
  @_container.html data
  @_loadMedium()

This function adds an entry to the browser's history and changes the container with the HTML coming from the server. It also instantiates the Medium Editor:

js
# app/assets/javascripts/blogr.coffee

_loadMedium: ->
  $('.editable').each (_, el) ->
    new MediumEditor("##{el.id}", placeholder: { text: el.placeholder })

Medium Editor has a multitude of options that you should check at their github page. We're using the placeholder option to show some text before creating the editors.

The new action of PostsController looks like this:

ruby
# app/controllers/blogr/posts_controller.rb

def new
  @post = Post.new
  render :edit
end

It renders the edit template that we saw earlier.

When the user starts writing the post, we create a new post, via this line of CoffeScript we mentioned above:

js
# app/assets/javascripts/blogr.coffee

@_document.on 'keyup', '#blogr-posts-form .editable', => @_create()

Let's now write the _create handler:

js
# app/assets/javascripts/blogr.coffee

_create: ->
  $('#blogr-posts-form').submit()
  @_document.off 'keyup'

We submit the form and stop listening to the keyup event. The form goes to the server via AJAX, and is handled by the create action:

ruby
# app/controllers/blogr/posts_controller.rb

def create
  @post = Post.new(post_params)
  @post.save
  render json: { 
    edit_url: edit_blogr_post_path(@post), 
    update_url: blogr_post_path(@post) 
  }
end

Here we save the post and send back the edit and update URLS, so that we can replace in the editing form. When the response is returned by the server, the function _onFormSuccess is called as we stated above:

js
# app/assets/javascripts/blogr.coffee

_onFormSuccess: (form, data) ->
  if location.pathname.match(/new$/)
    form = $('#blogr-posts-form')
    form.prop 'action', data.update_url
    form.prepend('<input type="hidden" name="_method" value="patch">')
    history.pushState {}, null, data.edit_url
  @_autoSave()

When we are creating a new post, and the URL ends in new, we change the form's action with the update URL and also the browser's URL with the edit URL. This way we enter the edit mode and auto save the post every minute.

Editing the post

This is done automatically, without a save button:

js
# app/assets/javascripts/blogr.coffee

_autoSave: ->
  clearTimeout(@_saveTimeout) if @_saveTimeout
  fn = -> $('#blogr-posts-form').submit()
  @_saveTimeout = setTimeout(fn, 1000)

As usual, we clear an existing timeout before calling a new one, and submit the form. This is now an editing form, and will call the update action on the server:

ruby
# app/controllers/blogr/posts_controller.rb

def update
  @post.update(post_params)
end

The post_params are simple:

ruby
# app/controllers/blogr/posts_controller.rb

def post_params
  params.require(:post).permit(:title, :body)
end

Publish a post

We can publish a post by clicking on the publish link. This posts to our controller action:

ruby
# app/controllers/blogr/posts_controller.rb

def publish
  @post.update(published_at: Time.now)
  redirect_to public_blogr_posts_url, notice: 'Post was successfully published.'
end

The unpublish does the opposite:

ruby
# app/controllers/blogr/posts_controller.rb

def unpublish
  @post.update(published_at: nil)
  redirect_to blogr_posts_url, notice: 'Post was successfully unpublished.'
end

Delete a post

Deleting the post is simple:

ruby
# app/controllers/blogr/posts_controller.rb

def destroy
  @post.destroy
  redirect_to blogr_posts_url, notice: 'Post was successfully deleted.'
end

The styles

Let's add a nice paint to our blogr, as per the classes we added to the HTML:

css
.blogr {
  font-family: sans-serif;

  a {
    border: none;
  }

  .blogr-cont {
    max-width: 960px;
    width: 95%;
    margin: 0 auto;
  }

  .posts-title {
    h1 {
      float: left;
      font-size: 2.5em;
      margin-bottom: 1em;
    }

    a {
      float: right;
      display: inline-block;
      color: #CE0A0A;
      border: 1px solid;
      border-radius: 2em;
      padding: .5em 1.4em;
      margin-top: 1em;
    }
  }

  .posts-nav {
    border-top: 1px solid #ddd;
    margin-bottom: 2em;

    li {
      display: inline-block;
      line-height: 5;
      text-transform: uppercase;
      font-size: .8em;
      letter-spacing: 2px;
      font-weight: 700;
      margin-right: 2em;
      margin-top: -1px;
      color: #222;
    }

    span {
      display: block;
      border-top: 1px solid #666;
    }

    a {
      color: #999;
    }
  }

  .blogr-posts {
    .summary {
      margin-bottom: 3em;

      h3 {
        a {
          font-family: serif;
          font-size: 2em;
          color: #222;
        }
      }

      .desc {
        color: #aaa;

        a {
          color: #aaa;
        }
      }

      .dot {
        position: relative;
        top: -.2em;
        display: inline-block;
        padding: 0 .3em;
      }
    }
  }

  .blogr-form {
    font-family: serif;
    max-width: 50em;

    textarea {
      display: none;
    }

    .title {
      font-size: 2em;
      color: #222;
      font-weight: bold;
      margin-bottom: 1em;
    }

    .body {
      font-size: 1.3em;
    }
  }
}

Conclusion

In this short series we saw how easy it is to add a blog to our existing Rails app using the Medium Editor. We could enrich it by adding and formatting links, images and other buttons and functionality that the Editor offers.