14: Using Textile Markup, plus In-Place Editing with Ajax




Learning Rails show

Summary: <h2>Goals</h2> <p>In this lesson we’re going to add two features to our content-management system: Textile markup and in-place editing. Both are, in principle, very simple to implement, thanks to a couple of Rails gems and plugins, as well as the <a href="/topic/24332-the-prototype-javascript-library">Prototype</a> and <a href="/topic/24333-the-scriptaculous-javascript-effects-library">Scriptaculous</a> JavaScript libraries. The reality turns out to be just as simple as we’d hope in the first example, and rather more complex in the second.</p> <p>Please note that, while we’ve tried to make these notes complete, <em>they aren’t the full tutorial;</em> that’s in the <strong>screencast</strong>, which you can access via the link on the left.</p> <h2>Setup</h2> <p>We begin with the code with which we ended Lesson 12. These zip files contain the beginning and ending states of the code:</p> <ul> <li><a href="/learningrails_13.zip">Learning Rails example app code as of the end of Lesson 13</a></li> <li><a href="/learningrails_14.zip">Learning Rails example app code as of the end of Lesson 14</a></li> </ul> <h3>Using Textile markup</h3> <p>We don’t want our administrative users to have to enter <span class="caps">HTML</span> code, so we’ll use a simple markup language called <a href="/topic/24246-using-textile-markup-in-ruby-on">Textile</a>.</p> <p>There’s two pieces of Ruby code we use to implement this. The first is the gem RedCloth, which you already have if you’re on Leopard. This is the code that translates Textile into <span class="caps">HTML</span>. The second is a Rails plugin called “acts_as_textiled”, which makes it trivially easy to add Textile markup to our content blocks.</p> <p>Run “gem list” to see if you already have RedCloth. If not:</p> <pre> sudo gem install RedCloth</pre> <p>To add the plugin, open a terminal window at the root of your application and enter the following command:</p> <pre> script/plugin install svn://errtheblog.com/svn/plugins/acts_as_textiled</pre> <p>Add to the Page model:</p> <pre> acts_as_textiled :body</pre> <p>That’s it. This plugin is smart enough to automatically display the Textile markup source when you’re displaying the body in a form field, as we are in the admin pages, but to render it into <span class="caps">HTML</span> anywhere else you use the body.</p> <p>You’ll need to stop and restart the server before the plugin will function.</p> <p>Now you can edit any of the pages in the content management system, such as the admin page, to convert the markup to Textile. Note that the page list view shows the <span class="caps">HTML</span> created by RedCloth — that’s because the acts_as_textiled plugin automatically renders the Textile content into <span class="caps">HTML</span> anyplace except in a form field.</p> <h2>In-place editing</h2> <p><a href="/topic/24251-implementing-an-in-place-editor-in-ruby">In-place editing</a> is a common feature of modern web applications, and there’s good support for it built in to Prototype. There’s also a Rails helper that wraps the Prototype method so we don’t even have to touch the JavaScript code.</p> <p>In Rails 1.2.x, the in-place editor helper was part of the framework. In Rails 2.0, however, it was split out as a plugin, in theory so it could be separately maintained. Unfortunately, it has not been maintained (as of this writing in early May 2008), so not only do we need to install the plug-in, we’re going to have to seek out and install a couple of patches before we have everything working.</p> <h3>Installing the plugin</h3> <p>Install the in_place_editing plugin by entering the following in a console at the root of your application:</p> <pre> script/plugin install http://svn.rubyonrails.org/rails/plugins/in_place_editing</pre> <h3>Modifying our code to use the plugin</h3> <p>The readme file provides basic installation instructions. We need to make two small changes to our code. In the file views/viewer/show.html.erb, replace the one line of text that is now in that file with the following:</p> <pre> &lt;%= in_place_editor_field :page, 'body' %&gt;</pre> <p>Then add this line to the top of the class in controllers/viewer_controller.rb:</p> <pre> in_place_edit_for :page, :body</pre> <p>And finally, we need to tell our application to load the JavaScript libraries. Add to the head section in views/layouts/application.html.erb:</p> <pre> &lt;%= javascript_include_tag :defaults %&gt;</pre> <p>Now restart the server, since we’ve installed a plugin, and everything should work as before. But now, when you click on any page text, a little edit box comes up!</p> <p>Try editing the text and click OK. Unfortunately, the save fails. To get a hint of what’s happening, view the page using Firefox with <a href="/topic/24276-debugging-ruby-on-rails-applications-using">Firebug</a> installed, and take a look at the console when you click the OK button. Scroll through the response and you’ll see that there’s an issue with the authenticity token.</p> <h3>The <span class="caps">CSRF</span> protection bug</h3> <p>The authenticity token is a feature added to Rails 2.0 to improve protection against cross-site request forgery (<span class="caps">CSRF</span>) attempts. Unfortunately, the team didn’t update the in-place editor plugin correspondingly, so this security feature trips it up.</p> <p>Try a google search on “in place editing <span class="caps">CSRF</span>”. The first result is a ticket in the Rails Trac titled “in_place_editing plugin does not work with <span class="caps">CSRF</span> protection”! Look at the patch suggested; focus on the forgery protection, grab the four lines of code and paste them into vendor/plugins/in_place_editing/lib/in_place_macros_helper.rb (see the screencast for details).</p> <p>After adding these lines restart the server, reload the web page, click on some text to edit it, and click OK to save. Now it works!</p> <p>A minor correction to the screencast discussion of the JavaScript code: in the audio, we referred to Ajax.InPlaceEditor as a Scriptaculous method. In fact, the Ajax object is part of the Prototype library, and the InPlaceEditor method is added by Scriptaculous.</p> <h3>Providing a larger text entry area</h3> <p>Next we want to provide a larger text entry field. Add the rows and columns options to the helper invocation:</p> <pre> &lt;%= in_place_editor_field :page, 'body', {}, {:rows =&gt; 20, :cols =&gt; 80} %&gt;</pre> <p>Refresh the browser, click on the text, and you should now see a decent size text area.</p> <h3>Displaying Textile source instead of <span class="caps">HTML</span> </h3> <p>Next problem: It is displaying the <span class="caps">HTML</span> markup, not the Textile source.</p> <p>By default, the in_place_editor_field helper pulls the text to be edited from the page itself, not from the server, so naturally it sees the rendered <span class="caps">HTML</span>. To fix this, we need to set an option to tell it to fetch the source from the server, and then we need to create the controller code to respond to that request.</p> <p>The option we need is called load_text_url. With this added to the helper invocation, we now have the following code in the view file:</p> <pre> &lt;%= in_place_editor_field :page, 'body', {}, {:rows =&gt; 20, :cols =&gt; 80, :load_text_url =&gt; {:controller =&gt; 'viewer', :action =&gt; 'get_unformatted_text', :id =&gt; @page.id}} %&gt; </pre> <p>This tells the Scriptaculous method to make an XMLHttp request (the workhorse of Ajax applications) to retrieve the unformatted text.</p> <p>Now we need to add the get_unformatted_text action to viewer_controller.rb:</p> <pre> def get_unformatted_text @page = Page.find(params[:id]) render :text =&gt; @page.body(:source) end </pre> <p>Take a look at the acts_as_textile readme to find the (:source) option that enables us to access the markup source, rather than the rendered <span class="caps">HTML</span>.</p> <p>Reload the page, and it now displays Textile, just as we wanted.</p> <h3>Do we want visitors to be able to edit the site?</h3> <p>One small problem — unless our goal is to provide a wiki, we probably don’t want any visitor to be able to edit our site’s contents. So we need to use the in-place editor only if someone is logged in. Simple enough, just change the view code to:</p> <pre> &lt;% if logged_in? %&gt; &lt;%= in_place_editor_field :page, 'body', {}, {:rows =&gt; 20, :cols =&gt; 80, :load_text_url =&gt; {:controller =&gt; 'viewer', :action =&gt; 'get_unformatted_text', :id =&gt; @page.id}} %&gt; &lt;% else %&gt; &lt;%= @page.body %&gt; &lt;% end %&gt; </pre> <p>Refresh the browser, and now in-place edit is available only to logged-in users.</p> <h3>Adding an external control</h3> <p>One more problem: in admin mode, we can’t access links on the page, because clicking anywhere in the page text takes us into edit mode. So our admin dashboard is now useless. This would also be a problem if any of the regular pages had links and we wanted to be be able to exercise them in admin mode.</p> <p>Looking back at the in-place editor helper file, we see an option for externalControl, so let’s try that. Add one more option to our in_place_editor_field invocation:</p> <pre> &lt;%= in_place_editor_field :page, 'body', {}, {:rows =&gt; 20, :cols =&gt; 80, :external_control =&gt; 'edit', :load_text_url =&gt; {:controller =&gt; 'viewer', :action =&gt; 'get_unformatted_text', :id =&gt; @page.id} } %&gt; </pre> <p>And create an edit link at the top of the page to trigger this:</p> <pre> &lt;a href='#' id='edit'&gt;Edit This Page&lt;/a&gt;</pre> <p>Refresh the page, and give the edit link a try. It works! Unfortunately, clicking on the text also triggers the edit control, so we need to somehow prevent that.</p> <h3>Making <strong>only</strong> the external control activate the edit mode</h3> <p>What we need is a way to have only the external control activate the in-place editor. Searching the web, we find confusing and contradictory results. But in the excellent book <a href="http://www.amazon.com/exec/obidos/ASIN/1934356018/buildicom-20">Prototype and Scriptaculous</a>, we read up on the in-place editor options, and find a reference to an option <code>externalControlOnly</code>. Alas, the Rails helper doesn’t support this, so it’s back to patching the code.</p> <p>In the helper, we find the line:</p> <pre> js_options['externalControl'] = "'#{options[:external_control]}'" if options[:external_control]</pre> <p>So let’s try duplicating this line in the helper, changing <code>externalControl</code> to <code>externalControlOnly</code> in the duplicate line, and then we change our view code to set this option to true:</p> <pre> &lt;%= in_place_editor_field :page, 'body', {}, {:rows =&gt; 20, :cols =&gt; 80, :external_control =&gt; 'edit', :external_control_only =&gt; true, :load_text_url =&gt; {:controller =&gt; 'viewer', :action =&gt; 'get_unformatted_text', :id =&gt; @page.id} } %&gt; </pre> <p>And presto, now it behaves as we want!</p> <p>Life in the open-source world is sometimes not as clean as we’d like it to be. Someone will no doubt update the in-place editor plugin soon, which will cut out more than half the effort of this exercise.</p> <h2>Coming Up</h2> <p>In our next lesson, we’re going to improve the navigation model for our content management system generated pages, so we can have sub-pages as well as main pages, and so the navigation button text can be different from the page title.</p>