Rendering Posts under a Topic
Learn how to render a set of posts nested underneath a specific topic and build the feature using behavior driven development.
Guide Tasks
  • Read Tutorial

In this lesson we're going to walk through the steps for building in the feature where a user will be able to navigate to a specific Topic page and be shown all of the posts nested inside of that topic. As a real world example, if you go to the news section on Reddit you will see all of the posts that are under the category of news.

Now that you know what we're going to build, let's get started by letting our topics factory know that it should have cases where it has multiple posts associated with it, we can do this by updating the factory like so:

# spec/factories/topics.rb

FactoryGirl.define do
  factory :topic do
    title 'Sports'

    factory :topic_with_posts do
      transient do
        posts_count 2
      end

      after(:create) do |topic, evaluator|
        create_list(:post, evaluator.posts_count, topic: topic)
      end
    end
  end

  factory :second_topic, class: 'Topic' do
    title 'Coding'
  end
end

This update let's the factory know that it may have a case where it has multiple posts associated with it, which, for the post_spec tests will be all of them. Opening up our post_spec let's add in the test cases that we want to build for this feature and include the first live spec:

# spec/features/post_spec.rb

require 'rails_helper'

describe 'post' do
  before do
    @topic = FactoryGirl.create(:topic_with_posts)
  end

  describe 'nested route' do
    before do
      visit topic_posts_path(topic_id: @topic)
    end

    it 'has an index page properly nested under a topic' do
      expect(page.status_code).to eq(200)
    end

    it 'renders multiple posts' do
      expect(@topic.posts.count).to eq(2)
    end

    xit 'has the post title' do
    end

    xit 'has the post description' do
    end

    xit 'has the post user name' do
    end

    xit 'has post links that point to post show pages' do
    end
  end
end

Here we're going to check on basic features associated with an index page, such as: making sure multiple posts are returned, ensuring the title is rendered, ensuring the description is shown, listing the post user name, and making sure that the post titles link to the post show page. Running rspec spec/features/post_spec.rb will let us know that it was looking for two posts but found 0, let's get that working. First update the index action in the PostsController. One key to working with nesting is knowing that you need to always call the parent element, so for this example our index action needs to know what Topic we're working with, we can find this by looking through the params and storing the topic_id in a variable and then using that to query all of the posts that belong to it:

# app/controllers/topics/posts_controller.rb

class Topics::PostsController < ApplicationController
  def index
    topic = Topic.friendly.find(params[:topic_id])
    @posts = topic.posts
  end
end

Now in our view we can print out those values:

# app/views/topics/posts/index.html.erb

<%= @posts.each do |post| %>
  <%= post %>
<% end %>

Running the specs now will show that they're all passing, let's make a small refactor, we know that we're going to need that Topic query for other methods in the post controller, so let's move it into its own private method, so the controller should now look like this:

# app/controllers/topics/posts_controller.rb

class Topics::PostsController < ApplicationController
  before_action :set_topic

  def index
    @posts = @topic.posts
  end

  private

    def set_topic
      @topic = Topic.friendly.find(params[:topic_id])
    end
end

Our tests are still passing, perfect. Moving onto the next spec, let's add in the expectation:

# spec/features/post_spec.rb

    it 'has the post title' do
      expect(page).to have_content('My Great Post')
    end

This test passes already because our last implementation already integrated this, however I thought it was important to include since it would be possible for the last test to pass (since it's only looking for the count), and this one to fail, the fact it's passing means we're good to go. The next spec is looking for the post description, let's add in this expectation:

# spec/features/post_spec.rb

    it 'has the post description' do
      expect(page).to have_content('Amazing content')
    end

This expectation actually passes too, but why? This is where you should sit back and think that maybe the tests or previous implementation weren't setup properly since we're not following the true TDD/BDD process, too many of our tests are passing. In this case the reason is because our view code implementation, if you look at the view we're actually printing out the entire object to the page, so the title, id, content, etc. are all showing up. Obviously this isn't the implementation we want, so let's clean up the view:

# app/views/topics/posts/index.html.erb

<%= @posts.each do |post| %>
  <%= post.title %>
  <%= post.content %>
<% end %>

The tests are still passing, but they're passing for the right reasons now. It's good to do a sanity test, don't simply rely on the test results themselves, make sure you're also checking the behavior in the browser to ensure the implementation is matching the goal. Now that we're back on track let's implement the next spec:

# spec/features/post_spec.rb

    it 'has the post user name' do
      expect(page).to have_content(@topic.posts.last.user.username)
    end

This spec is a little bit of an anti-pattern since it violates the Law of Demeter, which would say that we're using too much method chaining, our topic is listing its posts (that's fine), it's then calling a single post, then it's calling the user for that post, and then it's calling the username for that user. For right now we'll keep this, but it would be a good piece to come back and refactor later on. Running this spec will fail since our view isn't rendering any information about that the posts' user, so let's implement that:

# app/views/topics/posts/index.html.erb

<%= @posts.each do |post| %>
  <%= post.title %>
  <%= post.content %>
  <%= post.user.username %>
<% end %>

This works, perfect. Now let's create the final expectation:

# spec/features/post_spec.rb

    it 'has post links that point to post show pages' do
      expect(page).to have_link(@topic.posts.last.title, href: topic_post_path(topic_id: @topic, id: @topic.posts.last))
    end

This is checking the page to see if the posts are linking to their respective show pages, running these specs will give an error since it can't find the link, let's update that in the view code:

# app/views/topics/posts/index.html.erb

<%= @posts.each do |post| %>
  <%= link_to post.title, topic_post_path(topic_id: @topic, id: post) %>
  <%= post.content %>
  <%= post.user.username %>
<% end %>

Notice how we had to call the @topic value, whenever you're using nesting you need to ensure that you're always referencing the parent resource. Now if you run rspec you'll see all of the tests are passing, nice work! If you navigate through the site you'll see everything is working properly.

large

*Side note: * If you're having issues in the browser, make sure that the posts that you created in the console have a topic and user associated with them or you'll get an error, we'll be adding a validation for this later on.

Resources