In Rails the RESTful conventions based on resources don't stop with routes. Let's look at how this applies to our controllers.
Following what we specified in the routes, we'll create a PostsController
and a CommentsController
, moving the appropriate actions into them, and matching the action names to our new routes. The goal is to organize all the actions related to the posts
resource in the PostsController
and all the actions related to the comments
resource in the CommentsController
.
In Rails, the controllers are named with the plural form of the resources that they handle along with the name Controller
, therefore PostsController
and CommentsController
. They are stored in the app/controllers
directory, and the file names should be "snake cased" as posts_controller.rb
and comments_controller.rb
.
A typical Rails controller will have a subset of RESTful actions of index
, show
, new
, edit
, create
, update
, and destroy
. However, you may need non-RESTful actions, for example, you may need a way to publish a post. To do this you can also create a publish
action in your PostsController
and route to it manually. However, if you find yourself adding actions such as create_draft
, delete_draft
, update_draft
etc. to the PostsController
, it's a sign that you should think about having a DraftsController
instead, with create
, delete
and update
actions.
After this, ApplicationController
will be empty, but that's actually how it would look in a typical Rails app. These two new controllers will inherit from it and thus from ActionController::Base
, which is where they get all their Rails magic.
Here is an example of a controller action after this step:
class PostsController < ApplicationController
def show # <- used to be: show_post
post = Post.find(params['id'])
comment = Comment.new
render 'application/show_post', locals: { post: post, comment: comment }
end
end
If you look at what we have in the controller actions, you can see there's a pattern. We would create a model and use it to render the view templates - for example below:
class PostsController < ApplicationController
def show
post = Post.find(params['id'])
comment = Comment.new
render 'application/show_post', locals: { post: post, comment: comment }
end
end
This happens so much that Rails allows you to simplify this step with instance variables.
class PostsController < ApplicationController
def show
@post = Post.find(params['id'])
@comment = Comment.new
render 'application/show_post'
end
end
You have to update the post
and comment
variables in the view templates to @post
and @comment
as well.
Instance variables are automatically available in view templates, bound to the value that they are assigned to in the controller actions.
This doesn't mean you have to make all the variables in your controller actions instance variables, but for the ones you need in the view templates, make them instance variables and you won't have to manually pass them in anymore.
Currently, our view templates are still located in the app/views/application
directory, and when we render view templates, we have to explicitly specify where the template is:
class PostsController < ApplicationController
def show
@post = Post.find(params['id'])
@comment = Comment.new
render 'application/show_post'
end
end
If we move the show_post.html.erb
view templates to a directory with the same name of the resource - app/views/posts
, we can change the render
line to:
render 'show_post'
Rails will automatically search the app/views/posts
directory for views related to actions in the PostsController
.
What we want to do is use the RESTful conventions mentioned earlier. And if we name our view template as show.html.erb
, we can simplify our controller action further:
def show
@post = Post.find params['id']
@comment = Comment.new
end
That is, if we don't specify otherwise, Rails will automatically look for a view template in directory app/views/posts
(matches the controller resource's name) with the name that matches the controller action's name (in this case, show
). You can still, of course, explicitly tell Rails what view template to render, or set the response header with a redirect.
Let's make it so the rest of our actions in PostsController
follow RESTful conventions:
### app/controllers/posts_controller.rb ###
class PostsController < ApplicationController
# list_posts -> list -> index
def index
@posts = Post.all
end
# show_post -> show
def show
@post = Post.find(params['id'])
@comment = Comment.new
end
# new_post -> new
def new
@post = Post.new
end
# create_post -> create
def create
@post = Post.new('author' => params['author'], 'title' => params['title'], 'body' => params['body'])
if @post.save
redirect_to posts_path
else
render 'new'
end
end
# edit_post -> edit
def edit
@post = Post.find(params['id'])
end
# update_post -> update
def update
@post = Post.find(params['id'])
@post.set_attributes('author' => params['author'],
'title' => params['title'],
'body' => params['body'])
if @post.save
redirect_to posts_path
else
render 'edit'
end
end
# delete_post -> delete -> destroy
def destroy
post = Post.find(params['id'])
post.destroy
redirect_to posts_path
end
end
Notice now we are also using path helpers to define the redirection paths in the actions.
Next up, our Comments controller:
### app/controllers/comments_controller.rb ###
class CommentsController < ApplicationController
# def list_comments -> list -> index
def index
@comments = Comment.all
end
# def create_comment -> create
def create
@post = Post.find params['post_id']
@comment = @post.build_comment('author' => params['author'],
'body' => params['body'])
if @comment.save
redirect_to post_path(@post.id)
else
render 'posts/show'
end
end
# def delete_comment -> delete -> destroy
def destroy
post = Post.find(params['post_id'])
post.delete_comment(params['id'])
redirect_to post_path(post.id)
end
end
Once again, it is important to remember to update any views that rely on these
newly changed actions. For CommentsController
, that means we need to jump into
our view template, views/comments/index.html.erb
, and update it so that the
@comments
instance variable is used instead of the local comments
. This isn't too big of a change, so let's take care of it real quick.
<!-- OLD app/views/application/list_comments.html.erb -->
<!-- app/views/comments/index.html.erb -->
<div class="comments">
<% @comments.each do |comment| %>
<!-- ^ use `@comments` instead of `comments` -->
<div class="comment">
<small class="comment_meta">
<span class="comment_post_title">
Comment on
<strong><%= comment.post.title %></strong>
</span>
<span class="comment_author">by <%= comment.author %> -</span>
<em class="comment_created_at"><%= comment.created_at %></em>
</small>
<p class="comment_body"><%= comment.body %></p>
</div>
<hr />
<% end %>
</div>
We've covered some important Rails conventions in this chapter related to directory structure and our app's views. These conventions will be used going forward, so be sure to keep them in mind.