Conventional Views and Controller Actions

Restful Controller Conventions

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

Rendering Views With Instance Variables

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.

Controller Action and View File Structure

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.