Sunday, September 05, 2010

InPlaceEditor handling ActiveRecord validations in Rails

Last week I was facing an issue with Ajax InPlaceEditor in my Ruby On Rails application. The editor is very simple and when a page item is updated, the corresponding table is updated and the new value is updated on rendering. In my application i need to provide the user, an option to update his email id. Part of my controller

if @user.update_attributes(params[:name] => params[:value])
render :text => CGI::escapeHTML(@user.send(params[:name]).to_s)

It updates the database and renders the latest updated value if the active record validations are satisfied and the table is updated. I was worried about the else part i.e if the validations are failed, i should not update the table and the error message should be displayed with the original value retained in the page. So in this case i need to update two values, the original field value should be retained and the error value should be updated. In my controller i was having,

else
err_message = first_error(@user.errors)
render :update do |page|
page.replace_html params[:field_id].to_s, prev_value
page['notification_message'].hide
page.replace_html 'notification_message', :partial => 'shared/error', :object => err_message

I was shocked seeing some my field got updated with some chunk jvascript. I read InPlaceEditor docs and found that the field will get updated with the value returned from the controller. So in my case it takes my second page.replace_html as the returned value and updated the field. I googled and found many are having this issue but no one has mentioned any solution for this. Then I found that the script.aculo.us Ajax.InPlaceEditor is only designed to update one page field--the one that was edited. After going through the code, 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,(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.
So both of my replace_html in render update are actually working and the problem is only with the editors updater. So I called a new request to reload after that updater. so my modified script looks like

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);
new Ajax.Request(this.url, options);
} else {
var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
Object.extend(options, {

Now everything works great:) Hope this will be useful if you are facing similar problem with InPlaceEditor

2 comments:

Anonymous said...

Nice Find. :)


I was shocked seeing some my field got updated with some chunk jvascript.
I can totally hear that in Tamil in Vadivelu's voice ;)

Unknown said...

:) Naa appadiyae shock aittae :) really nice in vadivelu's voice