codahale.com٭blog

Coda Hale lives in Berkeley, CA, where he writes about Ruby on Rails, usability, web design and development, and the occasional bit about bicycles.

A Rails HOWTO: Simplify In-Place Editing with Script.aculo.us

Did Drew McLellan’s “Edit-in-place with Ajax” article over at 24 ways catch your eye, only to make you throw up in your mouth a little when you saw the PHP code? God, me too. One second, basking in the glow of Web 2.0 goodness, the next, assaulted by a total lack of distinction between model, view, and controller. Gaw.

But stil, in-place editing is sweet. Flickr makes good use of it, and it’s an example we should take note of. They don’t use it exclusively, but rather as a solution to the edit/show dichotomy (an unfortunate side effect of Rails scaffolding). Now, implementing this subtle magic in your application is easy as hell (as Drew’s article shows), but we’re Rails freaks. We hold ourselves to a higher standard. Here’s how to get your in-place edit on without mucking about with too much Javascript, using an awesome helper method I’ve cooked up.

First, let me talk about the controller architecture needed to do this. Once that’s done, we’ll move to the view code which makes this sucker fly. After that, I’ll explain how it all works and how to make it do even more wonderous things.

Our example application

Let’s cook up an example. I’ve always appreciated the profoundly surreal pedegogical approach popularized by why the lucky stiff (who is a shatter-brained, god-eating holy madman), so this example will be a bit odd. Let’s say we’re making a Rails app which allows robots to manage their lists of mortal enemies. We’ll have one controller, RobotHitLists, and the list will be generated in the robothitlists #index action:


class RobotHitListsController < ApplicationController

  def index
    @mortal_enemies = MortalEnemy.find(:all)
  end

end

You will also need to add the prototype.js and controls.js to your layout.

Adding an update action

Because we’re using AJAX to change things, we’ll need to write an action to make those changes.


  def update
    mortal_enemy = MortalEnemy.find(params[:id])
    mortal_enemy.reason_to_kill = params[:value]
    mortal_enemy.save
    mortal_enemy.reload
    render_text mortal_enemy.reason_to_kill
  end

This isn’t complicated at all. It finds the record in question, changes its reason_to_kill attribute, and then saves it. It reads it back from the database to make sure the change stuck (it’s really hard to have robust error feedback using this editing type, so validations aren’t dealt with well), and then renders the new value. The new value is passed as a parameter: :value. This is the default parameter name in the Script.aculo.us In-Place Editor, but it can be changed. (More on that later.)

A simple helper for a complicated task

Here’s a helper method I wrote to make life easier for us all:


 def editable_content(options)
   options[:content] = { :element => 'span' }.merge(options[:content])
   options[:url] = {}.merge(options[:url])
   options[:ajax] = { :okText => "'Save'", :cancelText => "'Cancel'"}.merge(options[:ajax] || {})
   script = Array.new
   script << "new Ajax.InPlaceEditor("
   script << "  '#{options[:content][:options][:id]}',"
   script << "  '#{url_for(options[:url])}',"
   script << "  {"
   script << options[:ajax].map{ |key, value| "#{key.to_s}: #{value}" }.join(", ")
   script << "  }"
   script << ")"

   content_tag(
     options[:content][:element],
     options[:content][:text],
     options[:content][:options]
   ) + javascript_tag( script.join("\\n") )
 end

Doesn’t look too complicated, but don’t let that fool you. It takes a hash as a parameter which has three keys: :content, which describes the (X)HTML element the content is stored in, as well as the content; :url, which contains the components of the URL the editor will post its results to when saved; and :ajax, which allows you to customize the editor and its behavior to your heart’s content.

Now, let’s get cracking.

The view code

In robot_hit_lists/index.rhtml, we have the following:


<h1>Mortal Enemies</h1>

<% unless @mortal_enemies.empty? %>
<ul>
  <% for mortal_enemy in @mortal_enemies %>
  <li>
    <%=h mortal_enemy.name %><%= editable_content(
      :content => {
        :element => 'span',
        :text => mortal_enemy.reason_to_kill,
        :options => {
          :id => "mortal_enemy_edit_#{mortal_enemy.id}",
          :class => 'editable-content'
        }
       },
      :url => {
        :controller => 'robot_hit_lists',
        :action => 'update',
        :id => mortal_enemy.id
       },
      :ajax => {
        :okText => "'is why I want to kill them'",
        :cancelText => "'Nevermind'"
       }
    ) %>
  </li>
  <% end %>
</ul>
<% end %>

And you’re set!

Whoah there, tiger–what the hell did you just do?

What, did I move too fast for you? Gotta keep up, sucka. There’s a lot packed into this method, and you can use it to implement just about anything you need. The trick is in the structure of the hash, and in order to use this function, you’ll need to know more about it. I told you it has three parts: :content, :url, and :ajax; here’s what they do:

:content

:content has three elements:

  • :element This is the element type the content will be embedded in. It defaults to span, but could just as easily be a div or any other (X)HTML element which has a CDATA section.
  • :text This is the text which is displayed at first. Usually the actual value of the attribute being edited, unless it’s a markup rendering situation (e.g., textilizing). More on that later.
  • :options These are the attributes of the element, in the :attribute => 'value' you should already be used to.
    • :id This is the id of the element and is required for this method to work. Can’t find an element without an id, dontchaknow.

:url

This contains the options for the update URL, and the format is the same as the parameters for url_for, link_to or any other URL-based helper in Rails. You need to specify the controller, otherwise this function won’t work (unless the controller is the default path).

:ajax

Now this is the bit you don’t know about. The Script.aculo.us In Place Editor can take a list of options, and this encapsulates that. If the value of one of these options is a string constant, you must add the single-quotes around it yourself (e.g., :cancelText => "'Nevermind'"). You can read all about the various parameters in the Script.aculo.us documentation.

You said something about Textile?

So what happens when what the user is editing is a markup language which needs to be rendered to be fully appreciated? You’ll need to change the controller around a bit:


class RobotHitListsController < ApplicationController
  include ActionView::Helpers::TextHelper

  def index
    @mortal_enemies = MortalEnemy.find(:all)
  end

  def update
    mortal_enemy = MortalEnemy.find(params[:id])
    mortal_enemy.reason_to_kill = params[:value]
    mortal_enemy.save
    mortal_enemy.reload
    render_text textilize(mortal_enemy.reason_to_kill)
  end

  def get_text
    mortal_enemy = MortalEnemy.find(params[:id])
    render_text mortal_enemy.reason_to_kill
  end

end

You’ll notice that update now returns the rendered text and we’ve added get_text which returns the actual markup.

Now, to the view:


<h1>Mortal Enemies</h1>

<% unless @mortal_enemies.empty? %>
<ul>
  <% for mortal_enemy in @mortal_enemies %>
  <li>
    <%=h mortal_enemy.name %><%= editable_content(
      :content => {
        :element => 'p',
        :text => textilize(mortal_enemy.reason_to_kill),
        :options => {
          :id => "mortal_enemy_edit_#{mortal_enemy.id}",
          :class => 'editable-content'
        }
       },
      :url => {
        :controller => 'robot_hit_lists',
        :action => 'update',
        :id => mortal_enemy.id
       },
      :ajax => {
        :okText => "'is why I want to kill them'"",
        :cancelText => "'Nevermind'",
        :loadTextURL => "'#{ url_for :controller => 'robot_hit_lists', :action => 'get_text', :id => mortal_enemy.id }'"
       }
    ) %>
  </li>
  <% end %>
</ul>
<% end %>

You can see that the content text is now the rendered text, and the :ajax options have a :loadTextURL parameter which refers to the new get_text action. The user will now see the rendered text, and when they click it, the control will retrieve and display the actual markup text.

Thanks

Not to toot my own horn (Oh, who am I kidding–it’s an article on my blog. Honk!) but I’m proud of this one. It wraps the less interesting aspects of in-place editing into a flexible, yet simple helper. Mind you, I’m just spending “countless hours rewriting [my] code to rejiggle [my] object hierarchy”, but it makes for more flexible, reusable code that’s easy to read and reuse.

You know… human stuff.

54 Responses to “A Rails HOWTO: Simplify In-Place Editing with Script.aculo.us”

  1. Andrew Says:

    This is fantastic, can’t wait to try it. Thanks!

  2. Andrew Says:

    Oh, and it might be worth pointing out that the :ajax hash can contain values for :higlightcolor and :hilightendcolor, as per the scriptaculous docs. It’s a little counter-intuitive that this formatting stuff is inside the :ajax attribute, but there you go.

    Anyway, that covers a chunk of the hilighting stuff that Drew’s code had to handle itself, which is great.

  3. Handlebar Sandwich » Blog Archive » McDowell Race Report Says:

    [...] Definitely check out the second guy there, I dig his style. In fact, in the last two posts he wrote up something about scriptaculous (which is what I used for the nifty little menu effects over on the right. click the titles, and you’ll see….), and in the next post he waxed poetic about his new surly fixie, which just looks downright sweet. Nice stuff. [...]

  4. Henry Says:

    Isn’t this already built into RoR with the ‘in_place_edit_for’ method?

  5. Coda Says:

    Henry–kind of. I can’t really use the built-in helpers for most of the things I do, because I don’t have a single parameter by which to identify the objects to be edited. For example, I have a project which has multiple stores and multiple products, and needs a description per product per store. So I need to pass the controller a store id and a product id, and then find the description which matches or, alternatively, create one if one doesn’t exist.

    That kind of stuff isn’t handled by the built-in helpers. Also, with the actual display, I much prefer to have a more hands-on approach w.r.t. the Script.aculo.us parameters, rather than encapsulate it.

    (I’m going to be writing a better in-place editing encapsulation for the controller and views, and when I do I’ll post it here.)

  6. Stephane Carrez Says:

    It’s really cool!

    I’ve improved this technic on the following point:
    - in the Ajax URL, I’m passing an additional parameter to specify to
    the update method which data member must be updated.

    - you have to be careful if the user clears the text or you show an
    empty field. With a the clickable area can become too
    small and you cannot edit that “empty” string. I’ve found that using
    a with a width or min-width solved the problem: the empty
    area can be edited.

    By the way, there is a typo in the :okText example, there is
    a spurious ” (double quote) at the end. You should read as:

    :okText => “‘is why I want to kill them’”,

  7. Coda Says:

    Thanks for the heads up, Stephane, both on the typo and the min-width issue!

  8. Ken Collins Says:

    When you say “because I don’t have a single parameter by which to identify the objects to be edited” what exactly do you mean? And object name or method for the in_place_editor_field method?

    I ask, becasue I was just learning about the “in_place_editor_field” tag when reading thru RAILs Recipes book. It has an excellent example on how to reflect those needed parameters. Not sure if that applies to your work. However, I do have a question that maybe you could help with. I just saw your new post and I am litterally in the “tall grass” here.

    What is “in_place_editor_options” for???? I was hoping after reading the script.aculo.us documentation that I could do something like this.

    ‘div’}, { :cancelText => ‘Never Mind…’} %>

    Am I close or is this not possible?

  9. Ken Collins Says:

    Whoops, that should be:

    ‘div’}, { :cancelText => ‘Never Mind…’} %>

  10. Coda Says:

    Ken: You need to declare it as a Javascript string, since I can’t tell what’s a Javascript function and what’s a string literal:

    
    { :cancelText => '"Never Mind..."' }
    
  11. Ken Collins Says:

    Actually, I did figure this out finally. I learned that the script.aculo.us “options” for Ajax.InPlaceEditor are different names in RAILS. So this does work now.


    'Never Mind' } %>

    I had a real hard time figuring that out since this page on the API docs lists these as the “options” for in_place_editor and did not mention that these work for the field as well. Or if it did, I missed it somewhere. But it seems with these options, I have access to most of the script.acul.us options using standard ruby key => value pairs.

    http://api.rubyonrails.com/classes/ActionView/Helpers/JavaScriptMacrosHelper.html#M000459

    It would be nice to see the default background fade to color there. Thoughts?

  12. automat_svet Says:

    thank you, this is really helpful!

    Any way to update a div after saving the field? I tried with the onComplete option but with no luck..

    Thanks again

  13. TreoPower Says:

    Thanks, this solved all of my AJAX problems. I too needed more control than the standard Rails method.

    I too ran into the zero-length field issue. I read that it can be resolved with width or min-width settings but I am not familiar with this mechanism. Are these the actual hash values and which part of the hash do they go in?

    Could someone post an example?

    Thanks!

  14. Matt Says:

    Great article, but I can’t get it to work. I get an error in the view file that says “undefined method `editable_content’.” I’m sure I messed up something somewhere, but can’t figure it out. Would you mind posting the complete code in a zip file? Thanks!

  15. Andy Says:

    You sir are wonderful. I have spent an age trying to get the in place editor to work and this method does the trick like a charm. As a ruby noob I was shocked to see it work so well first time. Anyhow back to the grindstone and thank you.

  16. Zach Inglis Says:

    This could have been a great tutorial if you actually didn’t try to be funny.

  17. Coda Says:

    Zach. Seriously. C’mon.

    You don’t like the content, go somewhere else. If someone’s got you tied down in front of the internet with your eyelids taped open, just say so and we’ll send the authorities ’round to sort things out.

    Act your age, whatever it is, and stop taking your personal frustrations out on other people in public venues. You’re just creating a paper trail of being an asshole.

  18. John Morgan Says:

    Great tutorial! Thanks for taking the time. I find that ‘trying to be funny’ spices up something that could otherwise be bland.

  19. Jose Says:

    This is a great tutorial on a really awesome function of ajax. Enjoyed it.

  20. Mare Says:

    @Matt

    I got the same error, but managed to fix it and, since I’m a complete newbie to Rails, am proud of it!
    “A simple helper” means that Coda actually created a helper. These things are placed in /app/helpers. So, edit “robothitlists_helper.rb” from that folder and add the code there.
    The “update action” goes into “robothitlists_controller.rb” (found in /app/controllers)

  21. Marc Says:

    I have looked all over the web and have not been able to find a suitable solution to the ‘empty field’ problem. Stephane eluded to a solution in comment #6, but did not provide an example. Would you mind?

  22. josh susser Says:

    After banging my head on this for a bit I’ve come to the conclusion that IPE doesn’t play nice with RESTful routes. I can’t figure out how to change the value param to a name like my_model[my_field]. I can get it to work with the URL and use a PUT when needed, but not being able to fix the attr name is a show-stopper.

  23. SciJacker.com :: get jacked on science! W00t w00t! » Why the Lucky Stiff Says:

    [...] Quoting Coda Hale: I’ve always appreciated the profoundly surreal pedagogical approach popularized by why the lucky stiff (who is a shatter-brained, god-eating holy madman). [...]

  24. blog.luguber.info » Blog Archive » Inline editing m/Rails Says:

    [...] Her er en kjapp liten artikkel om hvordan man med rails/ajax og script.aculo.us fikser inline editing. Når jeg finner ut hvordan jeg får ruby til å fange opp ikke-eksisterende metodekall og kalle nye metoder basert på navnet på de ikke-eksisterende metodekallene så blir dette superenkelt:) [...]

  25. jney Says:

    Good script thank you!
    modify and updating work, but after redirect i’ve got all my page include in the container div: page appears twice… :-S

    Can you help me?

  26. carlivar Says:

    I agree with comment 22 - I don’t see how RESTful routes and AJAX magic like this are compatible. An update to this article in the new REST world would be much appreciated…

  27. xybre Says:

    I just put :text => foo.text || "(no text)", to remedy the empty element problem. I’m trying to convince scriptaculous to send back more info to the controller about the value. Someone mentioned it, but didn’t explain. Bummer.

  28. xybre Says:

    Figured it out, add something like :column => "post" to the url_for method.

  29. Tom Says:

    To get around the REST errors, I tried modifying the Ajax.InPlaceEditor class in the prototype library for a quick fix. It creates a form that it submits in the background so I thought “Easy, I’ll just add that hidden field for the “put” method hack.” Nope it breaks prototype

    in function createForm I added:

    var hidden_put = document.createElement(”input”);
    hidden_put.name=”_method”;
    hidden_put.type=”hidden”;
    hidden_put.value=”put”;
    this.form.appendChild(hidden_put);

    This busted it (sob)

    Coda, give us your knowledge!! What are you on vacation or something?

  30. respondto Says:

    Where do I save the helper method?

  31. Daniel 'Buffi' Sundstrom Says:

    Hi!

    I’ve been fooling around with this excellent helper, and it works perfectly. However, I want to use the onComplete-thingee mentioned at Scriptaculous, but can’t get it to work. Everytime I add it to the :ajax-hash, the page loads ok but the in-place editor dies a horrible death.

    Please advise,

    Cheers!

    //Daniel

  32. Jon Garvin Says:

    I managed to get this to work very well with RESTful routes, and I think I even solved Josh’s problem. Here’s my view code. Notice the very last two options in the :ajax hash.

    {
    :element => 'span',
    :text => plan.assessment || 'enter your assessment plan',
    :options => {
    :id => "ubd_plan_edit_#{plan.id}",
    :class => 'editable-content'
    }
    },
    :url => {
    :controller => 'ubd_plans',
    :action => 'update',
    :id => plan.id
    },
    :ajax => {
    :okButton => "false",
    :cancelText => "'Cancel'",
    :ajaxOptions => "{ method: 'put' }",
    :paramName => "'ubd_plan[assessment]‘”
    }
    ) %>

  33. Josh Says:

    I used the following to generate a PUT request with an in place editor, however, I have not been able to change the parameter name yet:


    in_place_editor "my_dom_id",
    :url => "my_url",
    :options => "{ method:'put' }"

  34. Steven Says:

    I would like to echo the several requests above to *Please* expand on the “width or min-width” solution to the zero-length field problem.

    There are two problems with the “(no text)” fix. First, if the field contains an empty string (as opposed to a nil value), the fix as written doesn’t work. (Minor problem). The other problem is that if the user clicks on the field, then clicks OK, the dummy text gets entered into the database.

    I have tried applying length and min-length parameters to various components, and have had no luck in cleanly fixing this problem.

    Any help would be appreciated.

  35. Coolbox Says:

    Hello,

    Feeling like a bit of an idiot. I can’t get this to work at all. I am getting very frustrated as everyone else has managed to get it working so easily. The tutorial seems so easy and i’m sure it is…

    Just a couple of questions…

    The controller is called: RobotHitLists right? Does this not relate to the name of the database table?

    If this is the case then i am confused as to how you are using ‘mortal_enemy’ in the actions?! :-(

    Also, in the ‘Update Action’, how come ‘mortal_enemy’ does not have an ‘@’ before it at any point?

    I would be very grateful if you could send me the code for this example so i can see where i am going wrong, please?!

    Thank you very much and keep up the great work!

    Pete

  36. Jain Says:

    How can the maxlength of editable content be set…..

  37. kajinski Says:

    this ROCKS. Thanks for the awesome (and entertaining) walk-through. I’m using your excellent helper method on my site already.

  38. Evan Says:

    I Join to some of the previous comenters, how the editable text field width can be set? i cant find where to modify it and the fixed width its not useful sometimes with long text lines.

    anyway, very nice work, :)

  39. Chris Says:

    Question: I seem to have this working ok “Visually”, but for some reason my table is not updating when I change the field information. It says “saving…” and then it renders the original value. Has anyone had this problem?

  40. Generating a PUT request for an AJAX in place editor « i am josh Says:

    [...] some quick Googling, I found a comment from Josh Susser on the same problem - not being able to create a PUT request nor change the parameter name. For me, [...]

  41. Marshall from WineQ Says:

    Great work! I’m going to use this to let people edit their clubs and profiles on WineQ!

  42. Peter Boling Says:

    This is why Coda’s hot! Works for me, RESTfully!

    Here’s my stuff. The model in my example is UserPhoto:

    To the controller’s update method add:

    #find the requested object first, I use a filter which find UserPhoto based on params[:id]
    if request.xhr? #If AJAX request?
    @user_photo.update_attributes(params[:user_photo])
    render_text @user_photo.description
    else
    #… rest of method goes here for responses to non AJAX update calls:
    end

    To any view add:

    {
    :element => 'span',
    :text => user_photo.description || "Click to Enter Description...",
    :options => {
    :id => "user_photo_edit_#{user_photo.id}",
    :class => 'editable-content'
    }
    },
    :url => {
    :controller => 'user_photos',
    :action => 'update',
    :id => user_photo.id
    },
    :ajax => {
    :okButton => "false",
    :cancelText => "'Cancel'",
    :ajaxOptions => "{ method: 'put' }",
    :paramName => "'user_photo[description]‘”,
    }
    ) %>

  43. Peter Boling Says:

    it stripped out line containing the angle brackets:

    angle bracket%= editable_content(
    {
    :element => ’span’,
    :text => user_photo.description || “Click to Enter Description…”,
    etc…

  44. ed Says:

    Making a block editable:

    view.rhtnl

    'erail/report/report_lvsf' %>
    'erail/report/report_rwma' %>


    helper.rb

    def impressions(title, options = {}, &block)
    block_to_partial('erail/report/impressions', options.merge(:title => title), &block)
    end

    def block_to_partial(partial_name, options = {}, &block)
    options.merge!(:body => capture(&block))
    concat(render(:partial => partial_name, :locals => options), block.binding)
    end

    _impressions.rhtml

    {
    :element => 'span',
    :text => body || 'other text',
    :options => {
    :id => "erail_edit_#{@erail.id}",
    :class => 'editable-content',
    :rows => '10',
    }
    },
    :url => {
    :controller => 'erail',
    :action => 'update_impressions',
    :id => @erail.id
    },
    :ajax => {
    :okText => "'accept changes'",
    :cancelText => "'reset'"
    }
    ) %>

    I am thus able to make the entire block ( my partials ) editable, with
    :text => body. I update all this to a single database field ‘impressions’.

    There is some undesired behavior:

    1. The returned text after in_place_edit is not formatted

    2. I can in_place_edit only once, on second attempt, the editor field shrinks and only the first partial is displayed.

    3. I cannot expand the editor window; if I use ‘textarea’ as element, it starts to display html; I would like to avoid that.

    4. Conditions are need that decide whether the block or the updated editor field is displayed. Tricky.

  45. ed Says:

    'erail/report/report_lvsf' %>
    'erail/report/report_rwma' %>
    'erail/report/report_lvh' %>
    'erail/report/report_cardiac_chambers' %>
    'erail/report/report_valve_morphology' %>
    'erail/report/report_valve_function' %>
    'erail/report/report_diastole' %>
    'erail/report/report_asd_pfo' %>
    %>

    correction for previous post

  46. ed Says:

    view.rhtml

    ‘erail/report/report_lvsf’ %>
    ‘erail/report/report_rwma’ %>

    second attempt to correct, sorry.

  47. ed Says:

    I do not know why it cuts off the code I am trying to post:

    in my view I have ,
    then the block, which are partials that cotain form fields, followed by

  48. ed Says:

    The posting does not work please delete the last three, in my view I have impression ‘impressions’ do, then the block, actually partials and then the end tag to close it.
    The key is that I am passing the block as ‘body’ to :text.

  49. ed Says:

    solved my problems.

    I can now pass a lot of partials to a block, have the block used as :text => body value and edit as much as I want.

    My mistakes: my database field was tinytext that was too small, now I use mediumtext field type.

    I also changed the controller.js file and set the values for the columns to 120 and teh rows to 40. I know that you should be able to specify them elsewhere, but I used this way.

    The only thing I have to solve is how to return formatted text.

    I have now a report, at the botom of the report is a section called impressions; the content of that area is dynamically filled with my form field data.
    I get the form field data in a block, pass them to the partial _ impressions. In this partial we have the editing as described.

    I do my editing and update.

    The entire impressions text block is now stored in the field ‘impressions.

    I have a second final report page where the impressions section is made up by simply rendering the .

  50. ed Says:

    rendering the impressions

  51. leonardofaria.net → Ruby on Rails na caveira! Says:

    [...] In-place editing - sabe aquele recurso que tem pra todo lado e ninguém sabe como funciona? Aqui explica. [...]

  52. Pege Says:

    Anybody got in_place_editor_field working so that is saves on :onchange and cancels on :onblur ?

  53. Josh Says:

    Another approach to fixing the empty text problem described in #34 is to make a change to the helper, adding the following right before the content_tag:


    if options[:content][:text].nil? || options[:content][:text].lstrip.empty?
    options[:content][:text] = “(currently blank - click here to edit)”
    end

  54. eavaria Says:

    The helper didn’t work for me. In the join statement, the “\\n” added a literal “\n” to the string instead of a line break. Works perfectly for removing one \ so it reads:


    ) + javascript_tag( script.join(”\n”) )
    end

    sorry if it was posted. Didn’t read all the comments.