Sunday, June 28, 2009

Updating multiple form items with Ajax.InPlaceEditor

I have a few places where I want to do in place editing in my Rails app, so I tried using the official Rails in_place_editing plugin, but my form setup was just too complex for it to work. (From doing some reading up online, the plugin also has some other issues and hasn't been maintained. Because of this there are several alternative plugins out there.) But I eventually wound up finding a solution I could adapt and use.

So after implementing that, simple in-place editing worked beautifully! Unfortunately, simple in-place editing isn't quite enough for my app. When a page item is updated, sometimes I need it to update another item on the page. The script.aculo.us Ajax.InPlaceEditor is only designed to update one page item--the one that was edited. I tried working around it by changing something like this (in my controller action where I handled the AJAX call):

render :text => new_value

To something like this:

render :update do |page|
page.replace_html(original_page_id_to_update, :text => new_quantity)
page.replace_html(secondary_page_id_to_update, :text => new_value)
end

That didn't quite work. It updated the secondary_page_id_to_update element correctly, but instead of putting new_quantity into original_page_id_to_update, it put in something like this:

try { Element.update("original_page_id_to_update", new_quantity); Element.update("secondary_page_id_to_update", new_value); } catch (e) { alert('RJS error:\n\n' + e.toString()); alert('Element.update(\"original_page_id_to_update\", new_quantity);\nElement.update(\"secondary_page_id_to_update\", new_value);'); throw e }

I was stumped on this for quite a while. Spent a lot of time scouring The Google for answers, digging into the script.aculo.us documentation for Ajax.InPlaceEditor, and examining the actual Ajax.InPlaceEditor code. What my web searches turned up was that I'm not the only person with this problem--I found several forum posts on different sites from people trying to do the same, or similar, things. Unfortunately, none of them had a solution. :(

So I went for a run to take a break, then came home to take a shower and like Archimedes, I had a "Eureka!" moment. (Though his was in the bath.) I realized what was happening was that both of my replace_html calls were actually working, but as part of what Ajax.InPlaceEditor does it returns the final value of the render call, and sticks it into the field that was edited. This is just the default behavior, as it assumes you only want to update that one field.

Looking into the Ajax.InPlaceEditor code, I found this (relevant lines in bold):

handleFormSubmission: function(e) {
var form = this._form;
var value = $F(this._controls.editor);
this.prepareSubmission();
var params = this.options.callback(form, value) || '';
if (Object.isString(params))
params = params.toQueryParams();
params.editorId = this.element.id;
if (this.options.htmlResponse) {
var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
Object.extend(options, {
parameters: params,
onComplete: this._boundWrapperHandler,
onFailure: this._boundFailureHandler
});
new Ajax.Updater({ success: this.element }, this.url, options);
} else {
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {
parameters: params,
onComplete: this._boundWrapperHandler,
onFailure: this._boundFailureHandler
});
new Ajax.Request(this.url, options);
}
if (e) Event.stop(e);
}

What was happening was that it was creating a new Ajax.Updater, which is meant to update a single element with the return value from the url it calls, which turned out to be that chunk of Javascript above when I used the render :update block. The Javascript was being eval'd correctly, and updating both my fields, but then Ajax.Updater was re-updating the 1st field with the result of the render block, which was that chunk of Javascript. (BTW, I think this Rails bug is the same thing, but they didn't see that anything was being eval'd because they only had the one field being updated, so their guess was that the eval was failing.)

I tweaked the Ajax.InPlaceEditor code to replace the Ajax.Updater line with the Ajax.Request line, and suddenly everything worked! But that didn't seem like the right solution, so I looked at the logic in that block. It checks options.htmlResponse, so I tried setting :htmlResponse to false where I set up the Ajax.InPlaceEditor, and voila! It worked.

So hopefully this will be useful to people, and turns out to be very easy. Here's the summary:

  1. Use a render :update block with as many replace_html calls as you need (including one to update the in-place edit field itself).

  2. Set :htmlResponse to false when you set up your Ajax.InPlaceEditor.


And that's it!

2 comments:

Thomas said...

Thank you for posting this! I was using the same updated in_pace_editor, and knew there was some way to make it stop trying to update the field, but your post was the first to solve the problem - just threw :htmlResponse => "false" into the :ajax section of the editable_content. Thanks!

Walker said...

Great, glad it was useful!