Rails - link_to, routes and nested resources
nested-resources, routes, ruby-on-rails
Solution
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.
Problem
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?