Rails URL Helpers in Javascript with JsRoutes
When we use resource routing in Rails (paths like “/blogs/new” or “/blogs/2/edit”), we often use path variables such as new_blogs_path
. But, when when working in Javascript, we have to hardcode the the paths, i.e. “/blog/new”. In this article, we’ll cover JsRoutes, an awesome gem that lets us use a similar set of URL helpers in our Javascript code.
Why do we need JsRoutes?
The idea is that you should not have to construct URLs manually in Javascript if a library can do it for you! This becomes evident as soon as you start doing nested routes. For example, consider the following routing configuration:
resources :posts do
resources :comments
end
If we want the path to edit the comment with an ID of 4 that is associated with the post with an ID of 1, it gives “/posts/1/comments/4/edit”, which is a mouthful. It is much easier to use the URL helpers supplied by Rails than construct that string in our Javascript.
Setup
If you have the asset pipeline working in Rails, setting up JsRoutes is incredibly easy. Just add the following to the Gemfile:
gem "js-routes"
Then, run bundle install
. In any Javascript file (often, in application.js
), you can require JsRoutes by adding the following at the top of file:
//= require js-routes
If, as you follow along with this article, you find that JsRoutes is not loading in your Javascript, you might have to clear the asset pipeline cache:
rake tmp:cache:clear
Now, you should have JsRoutes ready to go. Let’s jump in and see some of the basics.
The Basics
Say we’ve added a new resource-based route in config/routes.rb
:
resources :posts
Each post has a title field and a content field. In PostsController
, we might have something like this:
class PostsController < ApplicationController
...
def create
@post = Post.new(:title => params[:title],
:content => params[:content])
@post.save
respond_to do |format| do
#probably should render something more sensible in a
#real application.
format.json { render :json => "{}"}
end
end
...
end
Notice that we aren’t returning any errors that occur in saving the post as part of the response. This is a bad idea in practice, but we’ll let it slide since this example isn’t really concerned with the data returned from the server. In the Javascript, suppose that we have a nice form to create a new post and we want to submit that form with AJAX. Breaking out the jQuery:
$.post(path, {title: $("#title_field").val(), content: $("#content_field").val()}, function(response) {
//do something w/ the response
});
But, how do we get the value of path
? That’s where JsRoutes comes into play:
var path = Routes.posts_path({format: "json"});
If you tried to console.log
the value of that variable, you’d see "/paths.json"
, as expected. It can be a little bit difficult to remember which JsRoutes calls correspond to which actions within the controller (although the names are nearly the same as the corresponding Rails helpers). One way to get a quick refresher is to add:
console.log(Routes)
to one of your Javascript files and examine the output. Another way is with this handy table:
Controller#Action | JsRoutes function call |
---|---|
posts#index | Routes.posts_path() |
posts#new | Routes.new_post_path() |
posts#create | Routes.posts_path() |
posts#show | Routes.post_path(id) |
posts#edit | Routes.edit_post_path(id) |
posts#update | Routes.post_path(id) |
posts#destroy | Routes.post_path(id) |
Notice that JsRoutes consists of function calls which means, in Javascript, that we have to have the calling parentheses (i.e. “()”), regardless of whether or not we have an “id” argument. Also note that some of the paths for different actions are the same (e.g. the paths for posts#index and posts#create). For these, the correct action is called based on the HTTP verb used (e.g. GET vs. POST).
Nested Routes
Alright, let’s take a look at how to handle nested routes with JsRoutes. Fortunately, if you’ve had experience with the Rails URL helpers, you’ll feel right at home. Let’s say we have this route configuration:
resources :posts do
resources :comments
end
The URL helpers are a bit more wordy but the ideas are the same:
Controller#Action | JsRoutes function call |
---|---|
comments#index | Routes.post_comments_path(post_id) |
comments#new | Routes.new_post_comment_path(post_id) |
comments#create | Routes.post_comments_path(post_id) |
comments#show | Routes.post_comment_path(post_id, comment_id) |
comments#edit | Routes.edit_post_comment_path(post_id, comment_id) |
comments#update | Routes.update_post_comment_path(post_id) |
comments#destroy | Routes.post_path(id) |
As a rule, if you’re passing an ID for a specific resource, use the singular form of that resource (e.g. “post”) and if you’re not passing an ID, use the plural form (e.g. “comments”).
The Magic of “to_param”
If you have URLs like “/post/138” in your application, SEO might be a bit difficult because, from the URL, it isn’t possible to tell any information (e.g. title or content) about the post the URL is referencing. That’s why Rails gives us to_param
. We can see it in action in our Post model:
class Post < ActiveRecord::Base
#...
#maybe verify uniqueness of title
def to_param
title.gsub(/ /, '_').downcase
end
#...
end
If we have a post with the title “JSRoutes is Awesome”, then we can now refer to this post as /posts/jsroutes_is_awesome
which offers a slight improvement in SEO. JsRoutes lets you use “to_param” in order to generate paths as well. Let’s check out an example with a flat resource:
var path = Routes.post_path({to_param: "jsroutes_is_awesome"}, {format: "json"});
That produces the path /posts/jsroutes_is_awesome.json
. This does mean that if you have a copy of the title of the post in the Javascript, you will have to reimplement the to_param
logic in Javascript, which is unfortunate but, more or less, unavoidable.
Configuring JsRoutes
There are a number of ways to configure JsRoutes, which can be pretty useful for specific situations. You can manage the configuration through an initializer, so create a new one in config/initializers/jsroutes.rb. Here’s how the general format of the initializer works:
JsRoutes.setup do |config|
config.option = value
end
Basically, you have a bunch of possible configuration options that you can set. Let’s take a look at some of them. One of the most useful for me has been:
JsRoutes.setup do |config|
config.default_url_options = {:format => "json"}
end
That takes all of the url helpers and by default, tacks on a “.json” at the end. If you’re writing a one-page application where most of your HTTP requests are actually fired by Javascript, you almost never load HTML, as most of your data will be in JSON. In that case, it makes more sense to set the default format to be JSON.
Another important URL configuration option is :exclude
:
JsRoutes.setup do |config|
config.default_url_options = {:exclude => [/^admin$/]}
end
That will exclude any “admin”-associated URLs from the Route object given to the Javascript code. The idea is that you might not want all the URLs in your project to be exposed to the client (when designing your app, however, it is advisable to assume that an attacker does have this list of paths). So, with the exclude
key, we can omit certain paths using regular expressions.
Say you always want full paths (e.g. “http://example.com/posts”) rather than local paths (e.g. “/posts”). That’s easy too:
JsRoutes.setup do |config|
config.prefix = "http://example.com"
end
Note that there is no trailing slash.
Wrapping it Up
Writing Rails code in the modern, Javascript-driven web requires a lot of mental context switching. With JsRoutes, at least you don’t have to rethink the way you handle paths when you switch to the front-end parts of your codebase.