Rails - link_to, routes and nested resources

knoopx picture knoopx · Oct 10, 2009 · Viewed 32.3k times · Source

As my understanding on nested resources, on edge Rails, should not

link_to 'User posts', @user.posts

point to

/users/:id/posts

?

The routes.rb file contains

map.resources :users, :has_many => :posts

If this is not the default behavior, can it be accomplished doing something else?

Answer

Ryan Bigg picture Ryan Bigg · Oct 11, 2009

Along the same lines as Rishav:

link_to "User Posts", [@user, :posts]

Here's an explanation from my blog.

Really early on in Rails, you would write routes like this:

redirect_to :controller => "posts", :action => "show", :id => @post.id

What this would do is dutifully redirect to the show action inside the PostsController and pass along the id parameter with a value of whatever @post.id returns. Typical 302 response.

Then Rails 1.2 came along and allowed you to use routing helpers, like this:

redirect_to post_path(@post)

And the people rejoiced.

This would do effectively the same thing. post_path here would build a route using the @post object that would look something like /posts/1 and then redirect_to would send back a 302 response to that route and the browser would follow it.

Then later versions (I can't remember which one), allowed syntax like this:

redirect_to @post

And the people rejoiced a second time.

Magic, but not really

Any sufficiently advanced technology is indistinguishable from magic.

While this seems like magic, it's not. What this is doing is actually very, very neat. The redirect_to method, much like its cousins link_to and form_for all use a common method to build URLs, called url_for. The url_for method takes many different varieties of objects, such as strings, hashes or even instances of models, like in the example above.

What it does with these objects then, is quite neat. In the case of the redirect_to @post call above, it inspects the @post object, sees that it is an object of the Post class (we assume, anyway) and checks to see if that object has been persisted in a database somewhere by calling persisted? on it.

By "persisted", I mean that a Ruby object has a matching record in the database somewhere. The persisted? method in Active Record is implemented like this:

def persisted?
  !(new_record? || destroyed?)
end

If the object wasn't created through a call such as Model.new then it won't be a new record, and if it hasn't had the destroy method called on it won't be destroyed either. If both of these cases are true, then that makes the object has most likely been persisted to the database in the form of a record.

If it has been persisted, then url_for knows that this object can be found somewhere, and that the place it can be found is most likely under a method called post_path. So it calls this method, and passes in the to_param value of this object which is usually the id.

In short, it's effectively doing this:

#{@post.class.downcase}_path(@post.to_param)

Which comes out to being this:

post_path(1)

And when that method is called you would get this little string:

"/posts/1"

Lovely!

This is called polymorphic routing. You can pass an object to methods like redirect_to, link_to and form_for and it will attempt to work out the correct URL of what to use.

The form of form_for

Now, when you're coding Rails you may have used form_for like this a very long time ago:

<% form_for @post, :url => { :controller => "posts", :action => "create" } do |f| %>

Of course, with advancements in Rails you could simplify it to this:

<% form_for @post, :url => posts_path do |f| %>

Because the form is going to default to having a POST HTTP method and therefore a request to posts_path is going to go to the create action of PostsController, rather than the index action, which is what would result if it were a GET request.

But why stop there? Why not just write this?

<%= form_for @post do |f| %>

Personally, I see no reason not to... if it's something as simple as this. The form_for method uses url_for underneath, just like redirect_to to work out where the form should go. It knows that the @post object is of the Post class (again, we assume) and it checks to see if the object is persisted. If it is, then it will use post_path(@post). If it's not, then posts_path.

The form_for method itself checks to see if the object passed in is persisted also, and if it is then it'll default to a PUT HTTP method, otherwise a POST.

So this is how form_for can be flexible enough to have an identical syntax on both a new and edit view. It's becoming more and more common these days for people to even put their whole form_for tags into a single partial and include it in both the new and edit pages.

A more complex form

So form_for is fairly simple for when you pass a normal object, but what happens if you pass an array of objects? Like this, for instance:

<%= form_for [@post, @comment] do |f| %>

Well, both url_for and form_for have you covered there too.

The url_for method detects that this is an array and separates out each part and inspects them individually. First, what is this @post thing? Well, in this case let's assume it's a Post instance that is persisted and has the id of 1. Second, what is this @comment object? It's a Comment instance that has not yet been persisted to the database.

What url_for will do here is build up the URL helper method piece by piece by placing each part in an array, joining it into a routing method and then calling that routing method with the necessary arguments.

First, it knows that the @post object is of the Post class and is persisted, therefore the URL helper will begin with post. Second, it knows that the @comment object is of the Comment class and is not persisted, and therefore comments will follow post in the URL helper build. The parts that url_for now knows about are [:post, :comments].

The url_for method combines these individual parts with an underscore, so that it becomes post_comments and then appends _path to the end of that, resulting in post_comments_path. Then it passes in just the persisted objects to the call to that method, resulting in a call like this:

post_comments_path(@post)

Calling that method results in this:

"/posts/1/comments"

Best part? form_for will still know to use POST if the @comment object is not a persisted object, and PUT if it is. A good thing to remember is that the form_for is always for the last object specified in the array. The objects prior to it are just its nesting, nothing more.

The more objects that are added, the more times url_for will do the hard yards and build the path out... although I recommend that you keep it to just two parts.

A symbolic form

Now that we've covered using an array containing objects for form_for, let's take a look at another common use. An array containing at least one Symbol object, like this:

<%= form_for [:admin, @post, @comment] do |f| %>

What the url_for method does here is very simple. It sees that there's a Symbol and takes it as it is. The first part of the url will simply be the same as the symbol: admin. The URL that url_for knows of at this point is just [:admin].

Then url_for goes through the remaining parts of the array. In this case, let's assume both @post and @comment are persisted and that they have the ids of 1 and 2 respectively. Same classes as before. url_for then adds post to the URL that it's building, and comment too, resulting in [:admin, :post, :comment].

Then the joining happens, resulting in a method of admin_post_comment_path, and because both @post and @comment are persisted here, they're passed in, resulting in this method call:

admin_post_comment_path(@post, @comment)

Which (usually) turns into this path:

/admin/posts/1/comments/2

You can use the array form of polymorphic routing with the redirect_to, link_to and form_for methods. There's probably other methods that I'm not remembering right now that can do it too... it's generally anything in Rails that would normally take a URL.

There's no need to build your URLs in any Rails version greater-than 2 using hashes; that's pretty old school.

Instead, experiment with your new knowledge of polymorphic routing and use it to the best of your advantage.