editable_content_tag : The in_place_editor for Rails 2.0
The in_place_editor was a nifty helper in rails 1.2 days, but has been removed from 2.0. This is mostly due to the fact that it was obsoleted by REST. It also doesn’t play well with the new CSRF (Cross Site Request Forgery) prevention measures that are default in Rails 2.0.
REST takes Rails’ “Convention Over Configuration” mantra to a whole new level, so we can leverage that to create a new, more flexible, in_place_editor helper function that requires almost no controller modifications to work.
Like the original, editable_content_tag uses Scriptaculous’ Ajax.InPlaceEditor which, by default, doesn’t play well at all with Rails’ new rest scheme.
Its prototype looks like:
new Ajax.InPlaceEditor( element, url, [options]);Where element is the id of the element you wish to edit in-place, the url is the url to which the update should be sent, and options is a hash which provides a myriad of… well.. options.
InPlaceEditor relies on the standard post method to send the updated value to the server. It then expects to receive a 200 OK response from the controller whose content is only the updated value of the attribute. This means that you must create a custom controller function for every attribute you wish to modify, or some clever switching based upon the modified attribute type. This won’t do.
Make Rails speak JSON
We solve this with two simple steps, first, we modify our RESTful update controller method to respond to a JSON request by serializing the object we’ve just updated, like so:
def update
redirect_to(@artist) unless @authorized
respond_to do |format|
if @artist.update_attributes(params[:artist])
#flash[:notice] = 'Artist was successfully updated.'
format.html { redirect_to(@artist) }
format.json { render :json => @artist } # <---- add this
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @artist.errors, :status => :unprocessable_entity }
end
end
endSo, upon a successful update, we receive a JSON serialized Artist object. We can then display the correct value by overriding the onComplete option with a new function in the options hash, and placing it in the call to the InPlaceEditor constructor:
onComplete: function(transport, element)
{ element.innerHTML=transport.responseText.evalJSON().property}"
/* property being the name of the attribute we're editing */Make Scriptaculous Speak REST
The second problem that InPlaceEditor presents is that its default behavior is to POST, when we wish to put. This can be solved by adding an item to the ajaxOptions hash inside the options hash:
ajaxOptions: {method: 'put'}Cake!
The third obstacle presented has to do with the parameters sent to the update request. The attribute parameter doesn’t follow Rails’ hash style params, also, we’re not passing through our authenticity_token which tells rails that this request came from a legit session. We can tackle those by modifying yet another option in the function call: callback:.
This option isn’t very descriptively named, but it references the “callback” function that InPlaceEditor calls when it is trying to build the query string that it passes in our new PUT request.
It looks like:
callback: function(form, value)
{ return 'authenticity_token=(big hex string)&artist[name]=value'; }Now when we are building this function call, we obviously must take care to pass in the correct values for the auth string, the object and the attribute, the value is provided to the callback function by InPlaceEditor.
So when building this call in our ruby helper, the complete function call looks like:
output = "
new Ajax.InPlaceEditor('#{options[:id]}', '#{options[:url]}', {
ajaxOptions: { method: 'put' },
callback: function(form, value)
{ return 'authenticity_token=#{form_authenticity_token}&#{objname}[#{prop}]=' + escape(value) },
onComplete: function(transport, element)
{ element.innerHTML=transport.responseText.evalJSON().#{prop};}"Putting it Together
So now that our javascript is out of the way, let’s build the rest of the helper. I add three option hashes, the 1st is the options for the content tag, so that items such as class and ID may be added or overridden. The 2nd hash, editOptions, allows options to be passed to the InPlaceEditor, the 3rd hash, ajaxOptions, allows options to be passed specifically to the ajaxOptions property in the InPlaceEditor.
I also add a boolean editable option so that I can clean up my view logic in deciding whether or not to make the field editable.
def editable_content_tag(elemtype, obj, prop, editable, options = {}, editOptions = {}, ajaxOptions = {})
objname = obj.class.to_s.downcase
options[:url] = "/#{objname.pluralize}/#{obj.id}" unless options.has_key? :url
options[:url] += '.json'
options[:id] = dom_id(obj)+"_#{prop}" unless options.has_key? :id
ajaxOptions[:method] = 'put'
edops = jsonify editOptions
ajops = jsonify ajaxOptions
tg = content_tag elemtype,
obj.send(prop),
options = options
if editable then
tg += "
<script type='text/javascript'>\n
new Ajax.InPlaceEditor('#{options[:id]}', '#{options[:url]}', {
ajaxOptions: { #{ajops} },
callback: function(form, value)
{ return 'authenticity_token=#{form_authenticity_token}&#{objname}[#{prop}]=' + escape(value) },
onComplete: function(transport, element)
{ element.innerHTML=transport.responseText.evalJSON().#{prop};}"
tg += ",#{edops}" unless edops.empty?
tg += "});\n"
tg += " </script>\n"
end
end
#stupid helper helper to convert a hash into a JSON options list
# (without the encompasing {}'s or any type of recursion
#Is there a rails API function that does this?
def jsonify hsh
str = ''
first = true
hsh.each do |k,v|
str += ', ' unless first
str += "#{k}: "
str += "'" unless (v.class == Fixnum or v.class == Float)
str += v.to_s
str += "'" unless (v.class == Fixnum or v.class == Float)
first = false
end
str
end
So that’s it! Just include these helper functions in your application.rb and make the 1 line change to your update function in your favorite controller, and viola!
In your view, the minimum params to invoke your helper are: the element type you wish to create, the object you wish to access and the attribute of the object you wish to access.
<%= editable_content_tag :span, @artist, 'name' %>For alternate options, such as class and text box size type options, the code looks like:
<%= editable_content_tag :div, @artist, 'profile', @authorized,
{:class => 'artist_name'}, {:rows => 8, :cols => 30}%>That’s it. Let me know how you would improve this, also, if there is interest, I can formally turn this into a plugin.
Posted in Ruby on Rails, Javascript | 11 comments | atom
3 days later:
Love the idea!
I didn’t know you could make InPlaceEditor do those things.
I think I will use this but would modify this so that it uses XML, more rails-ish to me.
4 days later:
This article was perfectly timed for me, thanks!
I got it to work for the first field on my page, but with two or more on the same page, they all get the same element id, which causes them all to not be clickable.
I added “options = { :id => “firstfield”} for the first, and “secondfield” for second and I get the yellow mouse over effect again, but there is another weirdness going on now. With manually setting the element ids, the original first field I had working is now the only one that is working. The second one will allow me to click and edit and shows the “saving …” but it doesn’t save. Upon looking in the console tail output, I can see that the first field is being submitted as a [put] but the second is [get] ?!?
I took a peek at the html source from within the browser and both are showing up identical except for their element ids. I then tried entering “ajaxOptions = { :method => “put”}” for the second, trying to force put method and it still submits as [get].
Since the second one is sending via get, the URL also gets chock full of all the content of all the fields on the page.
Any thoughts?
4 days later:
Upon further investigation, I cut out the first field, leaving the second one only. It still is being submitted using GET.
If I insert the first one and remove the second, it submits via PUT but the log also shows that it’s updating all the fields in the record, not just the one I edited. Shouldn’t it just update the one field?
6 days later:
Randy, strangely enough, it worked on my page (I’ve got like 6 of them, but you’re right, all tags did have the same ID, I fixed this)
6 days later:
Rick: Are all of them sending via PUT? I can’t for the life of me figure out why one sends via PUT and the other via GET when they are both identical code, except for adding the :id in options. It even shows “method = put” in the browser source.
6 days later:
I just found that It works fine in Firefox. I’ve been using Mac Safari 3.0.4 on OSX10.4.10.
I added three more fields (all fine in Firefox) and in Safari, they are working correctly via PUT. The one that still isn’t working is of type ‘text’ in mysql, but so is one of the other ones that is working fine. Could there be something about the surrounding tags that could be affecting this one editablecontenttag? I suppose I can tell my client to use firefox instead, but why would one be sending via GET and not the other four?
Needless to say, sending via GET does not update the text field in the RESTful controller.
8 days later:
an alternative to your callback solution:
in jQuery you can set the ajax, along with the authenticity token. I use a helper function which calls link_to_remote, then strips the ‘onclick’ javascript, and passes it to jQuery.ajaxSetup, which stores the parameters within the jQuery namespace.
I’m sure you can do the same with Prototype.
8 days later:
Randy: That is wierd. It sounds like a quirk of the InPlaceEditor on Safari. That is not a good thing. I only have linux boxes so my range of testing is not that broad :/
Darius: I like that idea, in fact, I prefer the way JQuery does things in almost all cases. I don’t know if Prototype offers that, I just used the example on their wiki page.
13 days later:
So what’s up with the options = options in editable-content-tag? Does that have some side effect I don’t know about, or is it a typo?
13 days later:
In the call to content_tag i’m specifying that the parameter named options is being passed a hash named options. It looks funny,but it works.
13 days later:
Is that different than :options => options? Anyhow, options is the third parameter of content_tag, so you should be safe to just pass it in.
Also, your ‘editable’ parameter doesn’t do what you’d expect: if you set it to false, the return value of the function will be nil rather than the tag value (because that block doesn’t get executed).
And I think you can use to_json on the hashes, like so:
edops = editOptions.to_jsonso long as you change the javascript that gets emitted by the helper so that it expects the curly braces.
Just stuff I ran into while working off your code. Thanks. =)