Enhancement in Rails remote_function() to support Javascript calls


If you have been on Rails in the past few months, you will know this programming environment is pretty productive. I managed to get my old PHP financial feeds site re-implemented in Ruby on Rails (RoR) in less than a week, whereas the original site tooks a month to do, and certainly does not look as glitzy as the new one.

I did however hit a stumbling block today while using the remote_function() method. This method is crucial to the implementation of remote AJAX calls, which allows for portions of your web page to be reloaded without having to reload the whole page. The limitation with the current Rails version of remote_function() is that it does not support Javascript calls at all. What does this mean ? Well, for most cases, it probably will not matter, as you simply want to invoke a controller action directly. However, if you ever want to do what I do, which is to perform an AJAX call to refresh my page partially whenever a selection is made on a dropdown list, then things get rather difficult.

A typical use of remote_function() is:

remote_function(:update => 'myid', :url => { :controller => 'my_controller', :action => 'do_something' })

which generates an AJAX call with the URL you specify expanded accordingly:

new Ajax.Updater('contents', '/my_controller/do_something', {asynchronous:true, evalScripts:true})

Which means the URL is static and hardcoded at the point of generation. So, if you have a dropdown list whose selection will not be known until much later, how are you going to append it to the URL in order to pass the selection to the my_controller controller ? You can't.

What you want to do is, instead of the hardcoded URL, pass in a Javascript wrapper function call, which extracts the selection and appends it to the URL:

url = url_for(:controller => 'my_controller', :action => 'do_something')
remote_function(:update => 'myid', :url => "callMyController('dropdown', url)"

This is however not possible because remote_function() always does this to anything you pass in for the URL:

function << "'#{url_for(options[:url])}'"

Which is no good because the generated AJAX call looks like this:

new Ajax.Updater('myid', 'callMyController('dropdown', '/my_controller/do_something')', {asynchronous:true, evalScripts:true})

creating first of all a syntax error because the single quotes around the URL are neutralised by our embedded single quotes. Secondly, even if we wash our Javascript call through the escape_javascript() function, the URL will then become:

new Ajax.Updater('myid', 'callMyController(\'dropdown\', \'/my_controller/do_something\')', {asynchronous:true, evalScripts:true})

Cool, not bad. The page compiles and displays correctly. However, when we make a selection on the dropdown list, Boom!.

Routing Error

Recognition failed for "/callMyController('myid','/my_controller/do_something')"

What has happened here is the Rails engine has treated the URL as a literal and  passed it directly to our server as is. No good.

So, to fix the problem, we need to enhance the above line of code in remote_function() to pass the Javascript wrapper call un-interpreted, unquoted, and un-escaped to the AJAX declaration.

url = options[:url]
# only resolve into Rails path if not a Javascript function
unless options[:js_url]
    url = "'" + url_for(options[:url]) + "'"
function << url

Perfect. Remember that we have defined a new :js_url option in order to indicate to the remote_function() call that we do not want any interpretation done on the value we passed in for the :url parameter. Now we can implement the whole solution for our dropdown list, passing in the new :js_url parameter (highlighted in red):

<% items = Items.find_all %>
<%= select_tag(
        :onchange => remote_function(
            :update => 'myid',
            :js_url => true,
            :url => "callMyController('dropdown','" + url_for(:controller => 'my_controller', :action => 'do_something') + "')"

function callMyController(id,url) {
    var dropdown = document.getElementById(id);
    var selected = dropdown.options[dropdown.options.selectedIndex].value;
    url = (url.match(/\?/) ? '&' : '?') + 'id=' + escape(selected);
    return url;

And the generated AJAX call is:

new Ajax.Updater('myid', callMyController('dropdown', '/my_controller/do_something'), {asynchronous:true, evalScripts:true})

Nicely done !!