h1ghlevelb1ts

Wysiwyg inline editing with tinyMce, jQuery, Rails and cucumber

I have been a little silent in this space cause I have been busy doing cool new stuff with rails. In a new brand project I am using couch (and nosql) for the first time and I am also diving into the jQuery ocean. It has been a rewarding trip this far with a couple of frustrations that has been solved with the help of the internets. Time to give something back!

This post is about how to use tinyMce not just as a nice wysiwyg editor but also for inline editing. The case is that you have a piece of markup on a page that you want to easily change without leaving the page. It turns out that it is not that hard for your specific case. A more generic case might be harder but not by much. As long as the thing you are editing is an entity that it stored in its entirety somewhere you should be good to go. (Couch is of course nice for this.)

I will assume that you have a running rails project and is familiar with the workings of rails. I am using rails 3 but I think most of the code should work with rails 2 as well. A running rails project with this setup can be found over at github.

Lets start with adding some gems to our Gemfile:

source 'http://rubygems.org'

gem 'rails'
gem 'sqlite3'

# user friendly markup
gem 'haml'

# wysiwyg editor
gem 'tiny_mce'

group :development, :test do
  gem 'cucumber-rails'
  gem 'capybara'
end

Now we are good to go. Now you may wonder what that cucumber thing is and it is included because we love tests a lot. We love them so much that we like to test drive our applications. Right! So lets start with a cucumber feature. To get rails and cucumber to work together run rails generate cucumber:install --capybara . This will add a bunch of folders, scripts and configurations and we can now call cucumber directly in our root folder to run all the features in the features folder. Lets create a feature called inlinerichediting.feature with something like this in it:

Feature: rich inline editing
  In order to get a comprehensive context
  as a web site editor
  I want to inline edit text on my pages

  Scenario: edit inline
    Given I am on the start page
    When I click on the element with id "fancy_text"
    And I fill in the editor with "A really fancy text"
    And I press "Save" within ".editor
    Then I should see "A really fancy text" within "#fancy_text"

If you know Cucumber you know that it is really the 5 last lines that actually does something. Of these we will need to add our own implementation to 2 of them and the other 3 are provided in web_steps.rb that the cucumber installer generated for us. I start with adding the 2 missing steps that cucumber helped me with writing. And of course nothing will work yet since we have added no views or controllers.... Lets do that - the controller is pretty straightforward:

class ContentController < ApplicationController
  def home
    @text = "Click me to edit the text"
  end
end

We will add more to this file later. And the first version of the view is even simpler:

.content#fancy_text
  = @text

A very simple layout/application.haml to go with this could be:

%html
  %head
    %title
      Inline Tiny MCE example
    = csrf_meta_tag
  %body
    = yield

And the first of our 5 cucumber steps now passes. In the second step we want to click on the text to make it editable. Capybara with Selenium as driver has support for Javascript behavior in the browser - this particular way to interact with the browser is not in the DSL however (only "normal" clickable things) so we need to get further down in the Capybara code to implement this step. What we need to do is find the element and then click on it. Like this:

When /^I click on the element with id "([^"]*)"$/ do |id|
  find_by_id(id).click
end

Just a silly line but I can tell you it took me some time to find out how to do it....

Now the second of our 5 cucumber steps passes but not the next which is both due to the next step not being implemented yet and to that we haven't actually done anything on click yet. Lets make the text clickable and show a text area on click. Lets introduce jquery.

jquery is the most used javascript library out there at the moment. It makes it easy to work with the DOM and has a rich ecosystem of all kinds of things. To add it we just download it and store it in out public/javascripts folder and add one line to views/layouts/application.haml :

= javascript_include_tag 'jquery'

We also need some scripts of our own and we want them in its own script file:

= javascript_include_tag 'application'

The first thing we want to do is a bit fancy. We want to give the user a feeling of that the content actually is clickable by changing its appearance when hovering over it. To do that we put the following lines into our javascript file:

// called by jquery when the document has loaded - sets up all de ztuff
$(document).ready(
  function(){
    $(".content").hover(
      function(event){
        $(this).addClass("selectForEditing");
      },
      function(event){
        $(this).removeClass("selectForEditing");
      }
    );
  }
);

So what happens here? A bit messy if you are not used to it. Essentially what happens is that when the document is ready - has finished loading - we have defined a function to be executed. In this function we assign listeners to certain events in the DOM model. In this case we want to listen to when the mouse enters the element with id "content" and when it is exiting the element. Whenever this happens we add or remove a CSS class from the element. Without the CSS - nothing happens. Lets add it and since we are doing everything the proper way we want to put it in its own file in the public/stylesheets folder. Lets call it style.css and put the following styling in it:

#content {
  border:solid;
  border-color:white;
  border-width:1px;
  padding:5px;
}

.selectForEditing {
  border:solid;
  border-color:gray;
  border-width:1px;
}

Ok - this is a little hack counting on that our background is white. You can probably do the same thing in some better way. Please tell me in the comments. CSS is not what I know best..... We also need to add CSS files to the header. The line

= stylesheet_link_tag :all

will do that for us. And a nice hovering effect is there!

Our next mission is to replace the text with an editor when clicked. Initially this will be a simple textarea tag and as a later step we will put a tinyMCE editor in there instead. Now we need some more markup! Our home.haml file now looks like this:

.content#fancy_text
  = @text
= form_tag '/content/save',           |
      :remote => true,                |
      :class => 'hidden editor' do    |
  = text_area_tag :text, @text, :class => 'mceEditor'
  = submit_tag 'Save'

You may be more familiar with the "form_for" tag but in order to avoid the database overhead and model classes for this tiny example I will use the "form_tag" helper instead. The parameters basically says that this is an asynchronous call to /content/save. The CSS "hidden" (set to display:none;) is applied to make this part of the markup invisible until the user clicks the text. The javascript to swap the text element with the form is:

$(".content").click( function(event){
  $(".content").hide();
  $(".editor").show();
});

and it should be placed in the document-ready-function as well. An editor is there but nothing can be submitted yet. Lets wait with that and make it in to a wysiwyg editor first. The rails plugin for TinyMCE comes with a couple of neat things. Firstly - to include it there is the include_tiny_mce_if_needed method to add to views/layouts/application.haml. It includes the TinyMCE files if they are needed - if the controller has stated a need for them. A controller includes TinyMCE with this code somewhere in the controller class:

uses_tiny_mce :only => [:home],
  :options => {
    :width => "600",
    :height => "500"
  }

Here it is possible to configure which of the controller actions that really need TinyMCE (or all if you want that). In our case we have only one controller method - so we add that one. The configuration of TinyMCE is also placed here if you want to have different configuration for different controllers. A default configuration can be placed in config/tiny_mce.yml.

So we have our fancy inline editor. All that is left is to make the tests run and submit our edits and return to the original text only page. So bear with me - there will be a couple of lines more of code before this is over. Firstly - lets try to make that third step in our cucumber suite run. The reason I waited until now is that the implementation of that step is TinyMCE specific. Thats a bit sad because it would be nice to have a generic test for whatever editor there is. Not possible with the way TinyMCE works. (At least not with my skills - please prove me wrong.) After a fair bit of trying and fair bit of internet browsing I found a solution that works although not elegantly. It uses a feature in selenium that makes it possible to run javascript in the browser. Here it is:

When /^I fill in the editor with "([^"]*)"$/ do |text|
  evaluate_script("tinyMCE.activeEditor.setContent('" + text + "');")
end

One thing left to make the third cucumber step work. We need to tell capybara to use selenium instead of running headless. In headless mode there is no support for arbitrary browser script running. To do this we add the file features/support/capybara.rb and enter Capybara.default_driver = :selenium and we have a running cucumber step that sets the text of a TinyMCE text area. A side effect is that selenium will start firefox to run the tests with. It will all slow down a bit.... and spork won't be that helpful either.

Actually the fourth step where we push the Save button also works but the resulting page contains an error so the final step can't find the entered text. In order to make this last thing work we will add the controller method that answers the remote call from the form and some javascript that handles updating of the page when the asynchronous call succeeds. Lets start with the controller. The form pointed towards '/content/save' so lets add a save method to the content controller.

def save
  @text = params[:text]
  # do some database saving here
  render :text => @text, :content_type => 'application/html'
end

And finally we need to run some ajax style scripts to take care of the result and make the page look good after the edit. When using ajax stuff the glue that makes rails agnostic to javascript framework needs to be present. I call the script file rails.js and add it just as the other javascript files above. Inside application.js we need to place some code to deal with the remote call to the server and - perhaps more importantly - the answer. It looks like this:

// hide the form when submitting - when the ajax calls succeeds
// put the returned data in the text field and show it
$(".editor").submit( function(event){
  $(".editor").hide();
}).bind("ajax:success", function(event,data,status,xhr){
  $(".content").html(data);
  $(".content").show();
});

...and should also be placed in the document-ready-function. The first part hides the editor div when submitting data. The actual susmission is done by normal form post so no need for scripting there. The second part is the callback when the remote call has succeeded. It sets the returned data in the original text field and then shows it.

Well - thats about it. I hope you enjoyed it! A running full code example can be found over at gihub. And a closing promise - I will try to post this kind of findings in parts in the future.....
 
comments powered by Disqus