homeASCIIcasts

296: Mercury Editor 

(view original Railscast)

Other translations: Ja

Mercury is a project by Jeremy Jackson that allows you to edit sections of an HTML page directly in the browser. You can see it in action by clicking “Test it Out” on the project’s home page. Doing so adds a toolbar to the top of the page and highlights certain sections of it. You can then edit these sections directly and save the changes back to the server. In this episode we’ll add Mercury to a Rails application.

Below is a page from a simple Content Management System. This application has a Page model and three page records each with a name and some content. Currently there’s no way to edit the pages so we’ll add Mercury to the app so that we can edit them directly.

Our Simple CMS Application.

Installing Mercury

The first step to installing Mercury is to go into the /Gemfile and add the mercury-rails gem. This gem is being developed fairly rapidly so we’ll grab a recent version directly from Github.

/Gemfile

source 'http://rubygems.org'
gem 'rails', '3.1.1'
gem 'sqlite3'
# Gems used only for assets and not required
# in production environments by default.
group :assets do
  gem 'sass-rails',   '~> 3.1.4'
  gem 'coffee-rails', '~> 3.1.1'
  gem 'uglifier', '>= 1.0.3'
end
gem 'jquery-rails'
gem 'mercury-rails', git: 'jejacks0n/mercury.git', ref: 'a2b16bcdc9'

The commit SHA of the version we’re using is included above so that you know which version we’ve used. As ever once we’ve changed the Gemfile we’ll need to run bundle to install the new gem.

Once the gem has installed we need to run Mercury’s installation generator. This will ask us if we want to install the layout and CSS overrides files and we do.

$ rails g mercury:install
      create  app/assets/javascripts/mercury.js
       route  Mercury::Engine.routes
Install the layout and CSS overrides files? [yN] y
      create  app/views/layouts/mercury.html.erb
      create  app/assets/stylesheets/mercury_overrides.css

The output from this command tells us that we need to run another command to install Mercury’s migrations so we’ll run that next and then migrate the database.

$ rake mercury_engine:install:migrations
Copied migration 20111108202946_create_images.rb from mercury_engine
noonoo:cms eifion$ rake db:migrate
==  CreateImages: migrating ===================================================
-- create_table(:images)
   -> 0.0017s
==  CreateImages: migrated (0.0018s) ==========================================

Our application now has a mercury.js file in its app/assets directory. This file contains all of the configuration options for Mercury and comments explaining them. We’ll come back to this file later; first we’ll address how this file is loaded. In a Rails 3.1 application this file will be loaded by default on every page as the application.js manifest file includes every file under app/assets/javascripts. Mercury is quite JavaScript-heavy so we only want it to load on the Mercury-editable pages. We’ll modify the manifest so that is only loads the files we specify. (As an alternative we could move the mercury.js file out to /vendor/assets instead.) When we’re editing a page Mercury will use its own layout file that includes this JavaScript file so though it appears that we’ve removed this file completely it will still be loaded when necessary.

/app/assets/javascripts/application.js

// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
//= require jquery
//= require jquery_ujs
//= require pages

We can do something similar for Mercury’s stylesheet but we won’t do that here.

Making Pages Editable

With Mercury installed we now have access to the Mercury editor from any page in our application. To put any page into edit mode we just need to add /editor between the URL’s host and path.

The Mercury toolbar.

Now that we know the URL to edit a page we can add the “Edit Page” link.

/app/views/pages/show.html.erb

<div id="header">
  <h1><%= raw @page.name %></h1>
  <ul id="navigation">
  <% Page.all.each do |page| %>
    <li><%= link_to_unless_current page.name, page %></li>
  <% end %>
  </ul>
</div>
<div class="content">
  <%= raw @page.content %>
  <p><%= link_to "Edit Page", "/editor" + request.path %></p>
</div>

Adding Editable Areas

We now have a link to edit any page but we haven’t yet defined any editable areas on the page. There are two sections we want to be editable: the page’s title and its content and each of these map to attributes in the Page model. To make part of a page editable we need to wrap it in an element that has a class of mercury-region and a data-type of editable. We also need to give each wrapper element an id so that we can identify it when the updated page is sent back to the server. We’ll call our two regions page_name and page_content.

/app/views/pages/show.html.erb

<div id="header">
  <h1><span id="page_name" class="mercury-region" data-type="editable"><%= raw @page.name %></span></h1>
  <ul id="navigation">
  <% Page.all.each do |page| %>
    <li><%= link_to_unless_current page.name, page %></li>
  <% end %>
  </ul>
</div>
<div class="content">
  <div id="page_content" class="mercury-region" data-type="editable">
    <%= raw @page.content %>
  </div>
  <p><%= link_to "Edit Page", "/editor" + request.path %></p>
</div>

When we reload the page now we’l see the two editable regions outlined in blue.

The page now has editable regions.

Saving Changes

We can edit the two regions as much as we like now and preview our changes but we can’t save any changes we make as we haven’t yet set up our application to be able to do that. When we click the “Save” icon Mercury will pop up an alert showing where it was trying to save the changes to.

Mercury show an alert if it can’t save changes to the server.

The URL that Mercury tries to save to is the page’s URL. Mercury sends a POST request to this URL containing the changes. We’ll change this so that the updated content is sent to a new mercury_update action on the PagesController. We’ll need to modify the routes file so that it knows how to handle this new action.

/config/routes.rb

Cms::Application.routes.draw do
  Mercury::Engine.routes
  root to: 'pages#index'
  resources :pages do
    member { post :mercury_update }
  end
end

Note that Mercury has added its own line to the routes file to enable the editor URL for each page.

Next we’ll write the new mercury_update action. This will need to find a page by its id then update it and return a response. For now we’ll just put a placeholder comment where the update code will go.

/app/controllers/pages_controller.rb

def mercury_update
  page = Page.find(params[:id])
  # Update page
  render text: ""
end

Now we need to tell Mercury that we’re not using its default URL for updating edited pages. Rather than hard-code this in JavaScript we’ll define it in a data attribute it the “Edit Page” link. We’ll also give the link an id now so that we can access it from JavaScript.

/app/views/pages/show.html.erb

<p><%= link_to "Edit Page", "/editor" + request.path, id: "edit_link", data: { save_url: mercury_update_page_path(@page) } %></p>

We can tell Mercury about this URL at the bottom of the mercury.js configuration file by adding the following code.

/app/assets/javascripts/mercury.js

$(window).bind('mercury:ready', function() {
  var link = $('#mercury_iframe').contents().find('#edit_link');
  Mercury.saveURL = link.data('save-url');
  link.hide();
});

This code binds to the mercury:ready event and when that event fires it finds the “Edit Page” link and sets Mercury’s saveURL to the value in the data-save-url attribute we added to it. Mercury loads the content of the current page into an iframe so we have to fetch its contents and find the link through there. As this code is triggered when the page is put into edit mode we’ll add a line to hide the “Edit Page” link here, too.

When we make some changes to the page now and try to save them the error message no longer shows which means that Mercury has submitted them to the server. If we look in the development log we’ll see that saving the page triggers the mercury_update action and that it has submitted a JSON string containing all the attributes necessary to update our Page model.

Started POST "/pages/1/mercury_update" for 127.0.0.1 at 2011-11-10 18:31:59 +0000
  Processing by PagesController#mercury_update as JSON
  Parameters: {"content"=>"{\"page_name\":{\"type\":\"editable\",\"value\":\"Welcome!!\",\"snippets\":{}},\"page_content\":{\"type\":\"editable\",\"value\":\"<p>In this ASCIIcasts episode we are going to look at the <a href=\\\"http://jejacks0n.github.com/mercury/\\\">Mercury Editor</a>. It allows you to edit a document in-place, right in the HTML. It works in the following browsers.</p>\\n<ul>\\n  <li>Firefox 4+</li>\\n  <li>Chrome 10+</li>\\n  <li>Safari 5+</li>\\n</ul>\\n<p>Try it out here by clicking on the <strong><em>Edit Page</em></strong> link below. There you will be able to change this page content and even the title above.</p>\",\"snippets\":{}}}", "id"=>"1"}

Mercury has the option to save the data as nested form parameters instead of by using JSON. To set this option we need to modify the mercury.html.erb file that was generated when we ran the Mercury generator. This file contains an options object with a saveStyle property. By default this property has a value of null which means that JSON will be used when a page update is sent back to the server. We’ll change this to 'form'.

/app/views/layouts/mercury.html.erb

<script type="text/javascript">
  var saveUrl = null;
  var options = {
    saveStyle: 'form',  // 'form', or 'json' (default json)
    saveMethod: null, // 'POST', or 'PUT', (create, vs. update -- default POST)
    visible: null     // if the interface should start visible or not (default true)
  };
  new Mercury.PageEditor(saveUrl, options);
</script>

When we save the changes to a page now they’re sent as nested parameters rather than as JSON data and we can use this data to save the changes to the database. The page’s name and content are both nested under the content parameter and each of these has a value property. We need these to save the page’s new content back to the database.

/app/controllers/pages_controller.rb

def mercury_update
  page = Page.find(params[:id])
  page.name = params[:content][:page_name][:value]
  page.content = params[:content][:page_content][:value]
  page.save!
  render text: ""
end

When we change the page now and save the changes nothing appears to happen but when we go back to the page we’ll see that the changes have been saved.

The changes are now saved.

We want our application to redirect back to the page once we’ve saved the changes and we can do that by listening for the mercury:saved event and redirecting when it fires.

/app/assets/javascripts/mercury.js

$(window).bind('mercury:saved', function() {
  window.location = window.location.href.replace(/\/editor\//i, '/');
});

Now when we save changes we’re redirected back to the page itself.

Going Further

There’s much more to Mercury than we can cover here. Reading through the comments in the mercury.js file will give you a good idea as to what is possible. For example we can customize exactly what appears in the toolbar; there are various features like snippets, history and notes that we can enable and a whole lot more. All of these are documented in mercury.js.

That’s it for our look at Mercury. It’s a great project and a lot of fun to use. If you want to add it to one of your Rails projects, though, bear in mind that you’ll need a modern version of Firefox, Chrome or Safari to use it.