Rails Named Routes from JavaScript

UPDATE 2: Read the latest information about the plugin.

UPDATE: Joshua Sierles has improved upon the concepts and code of this article and published an aptly named plugin: Named Routes in Javascript.

Last week a new JavaScript REST API called Jester was announced over at Giant Robots. It’s young, but I’m thoroughly impressed and am planning to put it thought its paces in a large project very soon. Go check it out if you’re using REST.

Alas, not all of my projects are using REST (yet). Even when developing following the REST pattern, you most likely still occationally use Named Routes. Also, RESTful routes are basically automatically generated Named Routes, so this technique can be used with REST (but may require enhancement to support all the various RESTful route variations).

The Giant Robots folks have inspired me to write about a technique I’ve been using to use Rails Named Routes in JavaScript to prepare Ajax requests using Unobtrusive JavaScript techniques.

A word of caution: The following approach is both simple and naive—it is not intended to be a complete implementation of Rails Named Routes in JavaScript. The technique hasn’t been extensively tested, but it meets my needs (so far). YMMV.

The Yucky Way

Consider the following Named Routes (defined in routes.rb):

ActionController::Routing::Routes.draw do |map|
    ...    
    map.with_options :controller => "story" do |story|
      story.create_story '/story/create/:milestone_id', :action => "create" 
      story.move_story '/story/move/:id', :action => "move" 
    end
    ...
end

One way to use a route on the client-side is to do something like the following in a view template:

<html>
...
<script type="javascript">
var move_story_url = '<%= move_story_url %>'
</script>
...
</html>

And then later use the move_story_url variable to piece together a URL for an Ajax request:

var story_id = ...
new Ajax.Request(move_story_url + "/" + story_id, {
  ...
})

Yuck. That defeats Rails’s routing goodness. Rails routes free the developer from putting URL’s and paths together by concatenating strings all over in their Ruby code. By putting URL’s and paths together by hand in JavaScript, the DRYness of defining a particular route in a single place is lost.

To further illustrate the problem, consider a route that has multiple identifiers:

http://localhost:3000/blog/:blog_id/posts/:post_id

Detailed knowledge of the route is needed in your JavaScript:

"http://localhost.com:3000/blog/" + blog_id + "/posts/" + post_id

Double yuck.

The Happy Way

First, include the following JavaScript file a view file (probably someplace global like application.rhtml):

<html>
...    
<script language="javascript" type="text/javascript" 
src="/named_routes_to_javascript"></script>
...
</html>

where the above referenced JavaScript file is dynamically generated from the routes defined in routes.rb, and contains a JavaScript function corresponding with each of your named routes.

Create a Controller to handle request for the dynamically created JavaScript file:

class NamedRoutesToJavascriptController < ApplicationController
  def generate
    headers['Content-Type'] = 'text/javascript'
    render :template => "named_routes_to_javascript/generate", :layout => false
  end
end

Create the Template to render the JavaScript (generate.rhtml):

var host = "<%= "#{request.protocol}#{request.host_with_port}" %>" 

<% ActionController::Routing::Routes.named_routes().each do |name, route| -%>
function <%= name %>_url(overrides) {
  var options = {
  <% route.significant_keys.each do |key| -%>
    <%= key %>: <%= route.defaults[key].nil? ? "''" : "'#{route.defaults[key]}'" %>,
  <% end -%>
    xxx: null
  }
  Object.extend(options, overrides || {});

  return host+"<%= route.segments.collect(&:to_s) %>"<% route.significant_keys.each do |key| -%>.replace(":<%= key %>", options.<%= key %>).replace("//", "/")<% end -%>
}
<% end -%>

Lastly, create the following route in routes.rb:

ActionController::Routing::Routes.draw do |map|
    ...
  map.named_routes_to_javascript 'named_routes_to_javascript', :controller => "named_routes_to_javascript", :action => "generate" 
  ...
end

The dynamically generated JavaScript looks like the following, with a function for each Named Route defined in routes.rb:

var host = "http://localhost:3000" 

function move_story_url(overrides) {
  var options = {
    id: '',
    controller: 'story',
    action: 'move',
    xxx: null
  }
  Object.extend(options, overrides || {});

  return host+"/story/move/:id/".replace(":id", options.id).replace("//", "/").replace(":controller", options.controller).replace("//", "/").replace(":action", options.action).replace("//", "/")
}

function create_story_url(overrrides) {
  // ...similar to above...
}

JavaScript snippets:

>>> move_story_url({id: 5})
http://localhost:3000/story/move/5/

>>> create_story_url({milestone_id:73})
http://localhost:3000/story/create/73

And finally, an Ajax request becomes:

var story_id = ...
new Ajax.Request(move_story_url({id: story_id}), {
  ...
})

No more knowledge of routes required within your JavaScript code—it just feels right.

Sorry, comments are closed for this article.