Rails-JavaScript Named Routes (Re)Visited
Almost a year ago I was unable to find a plugin that did something that I considered rather basic—allow for easy URL generation from JavaScript based on routes defined in Rails. I hacked up a proof of concept albeit rough solution and posted it to this blog. Delightfully, Joshua Sierles ran with the idea, improved upon it, and published a plugin.
A year later and thanks in part to the ease of collaboration provided by GitHub, I've formally paid my respects to Joshua's plugin by updating it for Rails 2.1, adding support for caching the generated javascript file, and a couple small features to make the javascript helpers behave a little more like the "real" Rails named route helpers.
What it does
Suppose that you had the following RESTful route defined in routes.rb:
map.resources :posts
Then, you'd get these (and other) familiar-looking javascript URL helpers:
posts_url() => "http://localhost:3000/posts"
post_url({id: 3}) => 'http://localhost:3000/posts/3'
post_path({id: 3}) => '/posts/3'
formatted_post_path({id: 3, :format, 'xml'}) => '/posts/3.xml'
The javascript generated by this plugin does not depend on any third-party javascript library.
Install
Assuming you are using Rails 2.1 RC1 or later, you can use the Rails script/plugin install command:
$ cd RAILS_ROOT
$ script/plugin install git://github.com/jsierles/js_named_routes.git
This plugin is targeted at Rails 2.1 and may not be compatible with other Rails versions without minor modifications.
Usage
At startup, a route for the plugin's all-important javascript generating NamedRoutesController is dynamically injected. With the plugin installed, visit the following URL to inspect the provided javascript helpers:
http://localhost:3000/javascripts/named_routes.js
Then, include the above javascript URL in your HTML head tag (probably in application.html.erb or some other layout file):
<%= javascript_include_tag :named_routes %>
And you're done. Go have some fun!
// Mootools 1.2b
new Request({url: post_comments_path({post_id: 4}) }).post({
post_id: 4, comment: {body: "something useful"}
});
// jQuery
$.post(post_comments_path({post_id: 4}), {
post_id: 4, comment: {body: "something useful"}
});
// Prototype
new Ajax.Request(post_comments_path({post_id: 4}), {
method: 'post',
parameters: {post_id: 4, comment: {body: "something useful"} }
});
Just like in Rails, specified route segments that aren't part of the route will be added as query parameters:
post_url({id: 3, extra: 'galen'}) => 'http://localhost:3000/posts/3?extra=galen'
post_path({id: 3, extra: 'galen'}) => '/posts/3?extra=galen'
This plugin uses page caching to cache the generated javascript routes, but lacks any intelligence for sweeping the cached file if you've made changes to your routes. That shouldn't be a problem if you are using Capistrano for production deployments because the cache is normally stored in RAILS_ROOT/public which is not kept across deployments. Caching is normally disabled in development mode, but if you turn on caching you may need to manually clear the cached file:
$ cd RAILS_ROOT
$ rm public/javascripts/named_routes.js
Alternatives
I'm pleased that there are at least two other plugins that also aim to satisfy the need to dynamically generate URLs based on Rails named routes: LessJsRoutes and JavaScript Routes.
LessJsRoutes offers similar syntax to js_named_routes. A notable difference is that the javascript routes file is built via a rake task and therefore needs to be manually re-built whenever you make a change to your routes. LessJsRoutes also depends on either jQuery or Prototype, but perhaps only if you take advantage of their handy looking _ajax helper methods (I haven't checked into this). They also support a more compact but still Rails-like syntax for specifying routes: post_url(3) rather than post_url({id: 3}).
JavaScript Routes (not to be confused with js_named_routes) has a very different and more verbose syntax. While I personally prefer simpler and more Rails-like syntax, the plugin appears to be able to generate routes other than "named routes." Like LessJsRoutes, it also relies on a rake task to manually build the javascript routes file.
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.