intertwingly

It’s just data

Rest In Place


Edit in place is a handy feature where clicking on an area of a web page replaces that portion of the page with a form which will enable the user to update that information.  At one time, this was a part of Rails, but in 2007 it moved out to a plugin, and improved upon.  The one that looked like it most closely matched my needs was REST in Place.  It comes in three flavors, I picked JQuery.

URLencoding

The first issue I had was that I would not be able to authenticate, despite carefully following the instructions for setting a rails_authenticity_token.  Looking at it from the server logs, it appeared that the token received had a space in it.  Looking at it from the client, I found a plus in the same place.  Looks like request wasn’t properly x-www-form-urlencoded. Looking at the source (line 18) verifies this to be the case.  A call to encodeURIComponent would solve this.  So would a call to CGI.escape in the assignment to rails_authenticity_token.  The former seems like the “right” way, but instead of modifying the gem, I decided to take the latter approach for now.

It is not clear to me why this problem wouldn’t affect others enough to have been noticed and fixed.

JSON

Once this was fixed, I could edit information in place.  The trouble is that the formatting was lost after the edit.  The information I was editing at the time was a dollar amount, something that I format with number_to_currency.  It seems that under the covers, after an update is made, the information is re-fetched using JSON and that’s the information that is displayed.  That information is “raw” and essentially straight from the database.  My first attempt to address this was to create new attributes which were not backed by the database; but by default such attributes are not included in the JSON so instead of simply losing the formatting, now I lost the result entirely.  This could be addressed, but I decided to go back to the original attributes, and centralize the formatting.

class ActiveRecord::Base
  def self.dollarize *attrs
    attrs.each do |name|
      define_method name do
        clone = SimpleDelegator.new(self[name])
        clone.extend(ApplicationHelper)
        def clone.to_s
          number_to_currency self, :unit=>''
        end
        def clone.to_json(opts)
          to_s.inspect
        end
        clone
      end

      define_method "#{name}=" do |value|
        self[name.to_sym]=value.gsub(/[^0-9.]/, '').to_f
      end
    end
  end
end

This allows fields to be declared as dollarized, after which point all formatting is taken care of automatically, and all other basic numeric functionality is delegated back to the original Float.

Kinda odd to have ApplicationHelper included within a model, but the amount of lines of code went down, processing became more consistent, so this was totally a net plus.

Caching

Now that the basic functionality worked, I noticed something odd.  If after doing an in-page-edit I clicked on a link and subsequently clicked on the back button, I was presented with JSON instead of the HTML page I was expecting.

I’m not totally sure what is going on here, as the server was properly sending private, max-age=0, must-revalidate as the cache control header.  The only thing I can figure is some oddity with Firefox 3.0.11 with XMLHttpRequeset and https sessions.  I’m not a big fan of conneg for this reason.  When it works, it is very useful.  When it doesn’t, it is often difficult to diagnose and debug.

But I have a known workaround.  By appending a question mark to the URI used for XHR, the client sees it as a different resource and the server processes it the same.

Helper

Wanting to apply the same workaround consistently across my application was the tipping point.  The markup rest_in_place wasn’t all that complicated, but was repetitive.  Time for a helper function:

# Usage:
#   <% rest_in_place(obj) do |rip| -%>
#     ... markup ...
#     <%= rip.editable :field %>
#     ... markup ...
#   <% end -%>

def rest_in_place(obj)
  yield RIP_Builder.new(url_for(obj), obj)
end

class RIP_Builder
  def initialize(url, obj)
    @url = url + "?"
    @obj = obj
    @name = ActionController::RecordIdentifier.singular_class_name(obj)
  end

  def editable(field)
    "<span class='rest_in_place' object='#{@name}' attribute='#{field}' url='#{@url}'>#{@obj.send field}</span>" 
  end
end

Attributes

One final note, only of interest to pedants.  The markup above is not conformant in that it invents new attributes.  In order to satisfy the markup orthodoxy, I’d suggest modifying RIP to accept synonyms for each attribute that happen to start with data-.  I would certainly use it on all public facing websites that made use of this feature.  As to what I use on my own private network, that’s between me and my server.