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:
:elementThis is the element type the content will be embedded in. It defaults to span, but could just as easily be adivor any other (X)HTML element which has a CDATA section.:textThis 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.:optionsThese are the attributes of the element, in the:attribute => 'value'you should already be used to.:idThis 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.
January 27th, 2006 at 1:06pm
This is fantastic, can’t wait to try it. Thanks!
January 27th, 2006 at 1:20pm
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.
January 29th, 2006 at 11:07pm
[...] 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. [...]
February 16th, 2006 at 9:33am
Isn’t this already built into RoR with the ‘in_place_edit_for’ method?
February 16th, 2006 at 6:48pm
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.)
February 28th, 2006 at 1:50pm
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’”,
February 28th, 2006 at 7:17pm
Thanks for the heads up, Stephane, both on the typo and the min-width issue!
April 3rd, 2006 at 12:41pm
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?
April 3rd, 2006 at 12:42pm
Whoops, that should be:
‘div’}, { :cancelText => ‘Never Mind…’} %>
April 3rd, 2006 at 6:23pm
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:
April 4th, 2006 at 4:25am
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?
April 11th, 2006 at 7:18pm
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
April 27th, 2006 at 8:04am
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!
May 27th, 2006 at 5:00pm
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!
July 18th, 2006 at 2:57am
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.
August 7th, 2006 at 5:17pm
This could have been a great tutorial if you actually didn’t try to be funny.
August 7th, 2006 at 8:33pm
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.
August 14th, 2006 at 9:46pm
Great tutorial! Thanks for taking the time. I find that ‘trying to be funny’ spices up something that could otherwise be bland.
September 7th, 2006 at 1:02pm
This is a great tutorial on a really awesome function of ajax. Enjoyed it.
November 5th, 2006 at 5:12am
@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)
November 8th, 2006 at 1:51pm
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?
November 17th, 2006 at 3:00pm
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.
November 25th, 2006 at 11:00pm
[...] 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). [...]
November 28th, 2006 at 5:45am
[...] 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:) [...]
December 15th, 2006 at 7:52pm
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?
January 13th, 2007 at 12:53am
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…
March 1st, 2007 at 2:58pm
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.March 2nd, 2007 at 7:28am
Figured it out, add something like
:column => "post"to the url_for method.March 7th, 2007 at 2:38pm
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?
March 19th, 2007 at 7:14pm
Where do I save the helper method?
April 4th, 2007 at 10:53am
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
June 1st, 2007 at 10:58am
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]‘”
}
) %>
June 18th, 2007 at 1:48pm
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' }"
June 28th, 2007 at 1:46pm
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.
July 11th, 2007 at 10:55am
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
July 19th, 2007 at 12:01am
How can the maxlength of editable content be set…..
August 17th, 2007 at 2:12pm
this ROCKS. Thanks for the awesome (and entertaining) walk-through. I’m using your excellent helper method on my site already.
August 18th, 2007 at 11:37am
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, :)
September 20th, 2007 at 1:27pm
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?
October 23rd, 2007 at 2:09pm
[...] 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, [...]
October 28th, 2007 at 10:16am
Great work! I’m going to use this to let people edit their clubs and profiles on WineQ!
November 19th, 2007 at 11:57am
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]‘”,
}
) %>
November 19th, 2007 at 11:59am
it stripped out line containing the angle brackets:
angle bracket%= editable_content(
{
:element => ’span’,
:text => user_photo.description || “Click to Enter Description…”,
etc…
November 20th, 2007 at 9:41pm
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.
November 20th, 2007 at 9:43pm
'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
November 20th, 2007 at 9:45pm
view.rhtml
‘erail/report/report_lvsf’ %>
‘erail/report/report_rwma’ %>
second attempt to correct, sorry.
November 20th, 2007 at 9:47pm
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
November 20th, 2007 at 9:51pm
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.
November 20th, 2007 at 11:02pm
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 .
November 20th, 2007 at 11:02pm
rendering the impressions
December 18th, 2007 at 5:48pm
[...] In-place editing - sabe aquele recurso que tem pra todo lado e ninguém sabe como funciona? Aqui explica. [...]
January 3rd, 2008 at 7:09am
Anybody got in_place_editor_field working so that is saves on :onchange and cancels on :onblur ?
February 16th, 2008 at 12:44am
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
March 14th, 2008 at 10:02am
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.