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 | 8 comments |