Restful In Place Editor
I was bothered that using the standard in_place_editor helpers for Rails required I add new actions to my controllers and corresponding routes in routes.rb.
As of Rails 2.0 it's is now a plug-in rather than part of core. I should probably make my changes a separate plug-in, or better, submit this as a patch. Instead, for now, I suggest you install the plug-in and copy and hack the code.
To install,
$ script/plugin install http://svn.rubyonrails.org/rails/plugins/in_place_editing/
Open the file vendor/plugins/in_place_editing/lib/in_place_macros_helper.rb and copy the two methods, in_place_editor and in_place_editor_field to your app/helpers/application_helper.rb
in_place_editor helper
We add a new option to this helper:
:as Name of the param the value is returned into
e.g. :as => 'foo[name]'
To the in_place_editor method, add the following line after the js_options['callback'] = line:
js_options['callback'] = "function(form) { return #{options[:with]} }" if options[:with]
# this line is the only change
js_options['callback'] = "function(form,value) { return '#{options[:as]}=' + escape(value) }" if options[:as]
in_place_editor_field helper
In this we make just 2 changes. First, the default :url will now be your standard :update action in the controller. And 2nd, the new value will be set using standard REST parameters, eg params[:foo => { attribute_name => value }]
# Renders the value of the specified object and method with in-place editing capabilities.
def in_place_editor_field(object, method, tag_options = {}, in_place_editor_options = {}) tag = ::ActionView::Helpers::InstanceTag.new(object, method, self)
tag_options = {:tag => "span", :id => "#{object}_#{method}_#{tag.object.id}_in_place_editor", :class => "in_place_editor_field"}.merge!(tag_options)
in_place_editor_options[:url] ||= { :action => "update", :id => tag.object.id, :method => :post, :_method => :put }
in_place_editor_options[:as] = "#{object}[#{method}]"
tag.to_content_tag(tag_options.delete(:tag), tag_options) + in_place_editor(tag_options[:id], in_place_editor_options)
end
Your controller
Last but not least you do things differently in your controller than the standard in_place_editor.
You do not need to use "in_place_edit_for" at all! Forgetaboutit!!
Rather, you just change the #update action to respond to ajax calls, as follows:
# PUT /foo/1
def update
@foo = Foo.find(params[:id])
success = @foo.update_attributes(params[:foo])
respond_to do |format|
format.html do
if success
flash[:notice] = 'Foo was successfully updated.'
redirect_to(@foo)
else
render :action => "edit"
end
format.js do
# assume updating only one attribute
attribute = params[:foo].keys.first.to_s
render :text => self.class.attributes.include? attribute ? @foo[attribute] : '(bad attribute)'
end
end
end
Basically we update the attributes in params, as usual. Except when its an ajax call, we assume there's really only one attribute in the params being updated. And the ajax renders the new value for updating the page.
A bit unconventionally, I do the update_attributes first then respond based on format. That's because, as mentioned in other blogs, there's no easy way to handle validations when doing in_place_editing. But then again it can be done (google it), and change your #update action as needed.
Views
There's nothing to change in the views, they work the same as the standard in_place_editor, for example
<%= in_place_editor_field "foo", :title %>
PS Thanks for the leethal comments... :)
UPDATE March 4, 2008
I found some views reference attributes better handled from a different controller than the current one, like associations of the current resource. I've modified the in_place_editor_field to use a controller based on the resource object name rather than assume the current controller.
I also added support for a :formatter option, so you can run the content through a filter before updating the view. For example, I use BlueCloth formatting, and specify :formatter => 'markdown' on my text attributes.
Here's the full helper:
def in_place_editor_rest(object, method, tag_options = {}, in_place_editor_options = {})
tag = ::ActionView::Helpers::InstanceTag.new(object, method, self)
tag_options = {:tag => "span", :id => "#{object}_#{method}_#{tag.object.id}_in_place_editor", :class => "in_place_editor_field"}.merge!(tag_options)
in_place_editor_options[:url] ||= { :controller => object.pluralize, :action => "update", :id => tag.object.id, :method => :post, :_method => :put }
in_place_editor_options[:as] = "#{object}[#{method}]"
# changed to support inline formatter
if formatter = in_place_editor_options.delete(:formatter)
in_place_editor_options[:load_text_url] ||= { :controller => object.pluralize, :action => 'show', :id => tag.object.id, :attribute => method.to_s }
var = @template.instance_variable_get("@#{object}")
value = var.send(method)
content = content_tag(tag_options.delete(:tag), @template.send( formatter, value), tag_options)
else
content = tag.to_content_tag(tag_options.delete(:tag), tag_options)
end
content + in_place_editor(tag_options[:id], in_place_editor_options)
end
Comments
>>
This is good, thank you! I'll leave you a lethal comment next time. :)
>>
Sven, i just found an alternative solution to mine, posted within days of this post, which may play better with the 2.0 forgery stuff http://www.bizmeetsdev.com/articles/2008/02/09/editable_content_tag
>>
I'm adding this note (found on the rails wiki) although I havent tried it or integrated it into the helper:
"In rails 2.0, if you’re trying to submit some crazy AJAX you’ve coded manually and you’re getting an Invalid Authenticity Token error, be sure to add the following to the query string of parameters being submitted:
&authenticity_token=<%= form_authenticity_token %>
form_authenticity_token will generate a valid token that rails needs to validate the request.
>>
I just tried adding the authenticity_token to the string as you described above, and it works perfectly now. A heck of a lot easier than the alternatives. Any known security issues?
Anybody else wonder why in_place_editor would be considered "crazy AJAX" ? Seems pretty mainstream to me...
>>
Nice patch, but there is a problem with multi-bytes characters in the value. It could be avoided by using encodeURIComponent() instead of escape(). - js_options['callback'] = "function(form,value) { return '#{options[:as]}=' + escape(value) }" if options[:as] + js_options['callback'] = "function(form,value) { return '#{options[:as]}=' + encodeURIComponent(value) }" if options[:as]
>>
Can you help explain the last line in your controller?
render :text => self.class.attributes.include? attribute ? @foo[attribute] : '(bad attribute)'
This doesn't even allow the page to load for me, and I'm trying to figure out what we're actually doing and what I need to change for my situation. I basically just changed @foo to @contact, but I think I'm missing something.
Re: Restful In Place Editor
Handling the data with RJS still raises the problem that after updating the value, the javascript of the RJS is displayed instead of the updated value. I tried to outline the solution to that at http://schotte.twoday.net/stories/5381459/
>>
SVN url is not working.
New Comment