Fun with in_place_editor

July 12th, 2006

You may or may not be aware of a Rails helper function called in_place_editor. This function generates a handy AJAXified form that displays text from a field in a normal, non-editable block – until the user clicks on the text. Magically, the uneditable text block becomes editable! (it’s hidden via javascript and a previously hidden form is displayed)

The in_place_editor is a quick fix for AJAXifying a field on a form and can be quite handy once it’s set up. After experimenting with it for a while, one major feature that I found lacking from this mini-helper was that data validation errors are not displayed.

In most cases, this probably wouldn’t be a problem, because in many text-areas, the sky is pretty much the limit. In the particular app I was working on at the time however, field size was important. For example, I had a few fields that were limited to 300 characters. In normal ActiveRecord model validation, if someone enters 301 characters and submits a form, the form will come back to them with the error message and the 301 characters still in their box, waiting to be dealt with.

Now try that with the in_place_editor, and instead, you’ll notice that if the record fails to save, you won’t get the customized error messages you get with non-ajax forms, and even worse, those 301 characters that your user poured over will have evaporated into the ether.

My solution was to use a session variable to store any unsaved field data. Since I had to do this for a number of fields in a table, I tried to DRY things up a little by creating a wrapper helper function for in_place_editor.

In these examples, our model is named Thing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ****** thing_controller *******
#
# get the actual text from the database 
def unformatted_text_from_field
  @thing = Thing.find params[:id]
  # we check here for the unsaved data in the session in case the
  # user's data was rejected by validation because we want
  # to give them a chance to redeem themselves and fix the data
  if session[:unsaved] and session[:unsaved][params[:field]]
    render :layout => false, :inline => session[:unsaved][params[:field]]
  else
    render :layout => false, :inline => "<%= @thing[\"#{params[:field]}\"]  %>"
  end
end

# our ajax receiving action
def update_thing_in_place
  @thing = Thing.find params[:id]
  @thing[params[:field]] = params[:value]
  if @thing.save
    if session[:unsaved]
      session[:unsaved][params[:field]] = nil
    end
  else
    # since the save failed, we need to store the unsaved field 
    # data into our session variable - notice we are using
    # a hash within the session - just in case they start to 
    # edit two different fields at once without saving, this 
    # hash will keep track of what data goes with what field
    if session[:unsaved].nil? session[:unsaved] = {}
    session[:unsaved][params[:field]] = params[:value]
  end
# falls through to RJS template
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ***** RJS template (update_thing_in_place.rjs) *****
#
# show errors from validation
if error_messages_for('thing')
  page["errors_for_#{params[:field]}"].replace_html error_messages_for 'thing'
  page["errors_for_#{params[:field]}"].show
else
  page["errors_for_#{params[:field]}"].hide
end
# put back original text so it's not lost after getting rejected by validator
if session[:unsaved] and session[:unsaved][params[:field]]
  page["#{params[:field]}"].replace_html textilize_without_paragraph(session[:unsaved][params[:field]])
else
  page["#{params[:field]}"].replace_html textilize_without_paragraph(@thing["#{params[:field]}"])
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ***** in a helper somewhere ******
# (like helpers/application_helper.rb)
#
#
# creates an in_place_editor for given fieldname 
# options are rows, cols, save_text (for save button), edit_text (for text in place of empty data)
def in_place_field(object, fieldname, label_text = nil, editor_options = {} )
  # default options
  editor_options[:rows] ||= 3
  editor_options[:cols] ||= 80
  editor_options[:save_text] ||= "save"
  editor_options[:edit_text] ||= "click here to edit"
  editor_options[:show_char_count] ||= false
  
  output = ""
  output << "<div id=\"errors_for_#{fieldname}\"></div>"
  if label_text 
    output <<  "#{label_text}: "
  end
  
  output <<  "<div class=\"in_place_edit_box\" id=\"#{fieldname}\">"
  
  if object[fieldname] and object[fieldname].size > 0  
    output << textilize_without_paragraph(object[fieldname]) 
  else
    output << "<em>#{editor_options[:edit_text]}</em>"
  end
  output << "</div>"
  output << in_place_editor( fieldname, { :load_text_url => { :action => :unformatted_text_from_field, :field => fieldname, :id => object }, 
        :url => { :action => "update_thing_in_place", :field => fieldname, :id => object },
        :rows => editor_options[:rows], :cols=> editor_options[:cols], :save_text => editor_options[:save_text], :script => true } )
  
  output
end
Now in our view, we can use this one-liner to create in_place_editors for any field our model has.
1
2
3
<div>
  <%= in_place_field(@thing, "highlights", "Notes/Highlights") %>
</div>
Notice in our wrapper helper, I'm passing any options on to in_place_editor. So, for example, if you want to limit the text-editing box to a one-line text field (rather than a multiline textarea) you can set :rows => 1 in the options of in_place_field. Next steps for this helper function? The most obvious to me would be to extend it to accept fields from any model, instead of being designed just for Thing. Shouldn't be that far off, though, eh?

1 Response to “Fun with in_place_editor”

  1. srinivas Says:

    hi i have a doubt regarding in place editors; when the value of the field is null i am not able to access the editor field. How can i avoid this? Is this a bug or there is something which i am missing here?

Leave a Reply