- Read Tutorial
In this guide we will walk through an initial implementation of the homepage design. I'm not a huge fan of the homepage design, it looks too much like a news site instead of a social media application, so we're going to use the same layout that we integrated into the Topic
page for the homepage.
Now if you're first inclination is to copy and paste the code from the posts
index
page, stop! Remember we need our code to be DRY, so we can't have duplicate code. This is a perfect situation to refactor our posts
view code into a partial that can be shared.
If you look at the posts/index.html.erb
file you'll see that it has a call to the @posts
instance variable and it would make sense for the homepage to have the same call, so the only behavior that will be different is that the homepage won't have posts nested underneath a Topic
, and we can control the data in the controller, are you seeing now why it's so important to have every MVC element perform its job and nothing more? If our view code communicated with the model we wouldn't be able to share it between different parts of the application.
So let's start by creating a partial to store our posts:
touch app/views/shared/_posts.html.erb
Now let's move everything in the @posts.each
block to the partial, I'm going to keep the HTML wrapper in the file since we may want to have some different style options on the homepage compared with the Topic
page. So our partial should now look like this:
<!-- app/views/shared/_posts.html.erb --> <% @posts.each do |post| %> <li class="clearfix"> <div class="ratings"> <a href="#up" class="up"></a> <h1>1178</h1> <a href="#down" class="down"></a> </div> <div class="article-image"> <div class="mask"></div> <img src="https://s3.amazonaws.com/rails-camp-tutorials/pro-rails/ui/article_image.png"/> </div> <div class="article-desc"> <h2><%= link_to post.title, topic_post_path(topic_id: @topic, id: post) %></h2> <p><%= post.content %></p> <span>By <strong><%= post.user.username %></strong> <%= post.created_at %></span> </div> <div class="article-stats"> <ul class="pull-right"> <li> <span class="icon icon-view"></span> <h2>347</h2> </li> <li> <span class="icon icon-comment"></span> <h2>347</h2> </li> <li> <span class="icon icon-connection"></span> <h2>347</h2> </li> </ul> </div> </li> <% end %>
And our posts/index
template will look like this:
<!-- app/views/topics/posts/index.html.erb --> <div class="container"> <h1><%= @topic.title %></h1> <div class="row"> <div class="col-xs-9 pull-left"> <ul class="articles"> <%= render 'shared/posts' %> </ul> </div> </div> <%= link_to "New Post", new_post_path(topic_id: @topic.id) %> </div>
First and foremost, doesn't that index
template already look better? Even if we weren't going to be using the posts
in another part of the application this would still be a good refactor to do simply to clean up the file. Now if you run rspec
you'll see that all of the tests are still passing and if you navigate to the site in the browser you'll find that everything is working just like before, so all good so far.
How to Implement the Homepage Design
Now let's start by creating some Capybara integration tests to establish some expectations for what we want to see on the homepage.
We already have a placeholder test that we can keep there for regression purposes, however it looks like I have the describe
block names mixed up, so let's fix that first:
# spec/features/static_spec.rb require 'rails_helper' describe 'homepage' do describe 'navigate' do it 'can be reached successfully' do visit root_path expect(page.status_code).to eq(200) end end end
That's mainly for documentation and code organization purposes since it wouldn't make sense to have all of the specs nested inside a navigate
block. With that squared away let's think through what what want on the homepage:
- We want a list of topics
- We want multiple posts from various topics
Eventually it would make sense to apply a custom popularity algorithm to the homepage so that the popular posts are shown, but for a new application we're simply going to show the most recent posts from all of the topics
.
Let's first build a spec for ensuring that a list of the topics
show on the homepage:
# spec/features/static_spec.rb describe 'content' do it 'has a list of topics' do FactoryGirl.create(:topic) FactoryGirl.create(:second_topic) visit root_path expect(page).to have_content("Sports") expect(page).to have_content("Coding") end end
Here we're creating both of our topics from our topics
factory and we're expecting to find both of the names on the homepage. Running this spec will fail since our homepage is empty, so let's implement the most basic implementation to get this test passing, opening up the view template put in the code:
<!-- app/views/static/home.html.erb --> <%= Topic.all.inspect %>
Woah!!! Is that a model call in the view?! Yes, remember the process of Behavior Driven Development is to first implement the most basic solution to get the test passing, and then refactor it. There is a method to the madness, if we went through the process of making changes to the controller and view, it may take us longer to debug the root of the problem since the issue could deal with the code flow, view layer, or test. Here we know that if there is an issue it's most likely with the test because code such as <%= Topic.all.inspect %>
is so basic. If you run rspec spec/features/static_spec.rb
you'll see that our test is passing, so now we can refactor it.
Let's start by making a call to the Topic
model in the controller for the home
action:
# app/controllers/static_controller.rb class StaticController < ApplicationController def home @topics = Topic.all end end
So we can call the instance variable from the view:
<!-- app/views/static/home.html.erb --> <%= @topic.inspect %>
Running the tests again will show that the tests are still passing through the first refactor. If you startup the rails server and look at the homepage you'll see it's printing out the full hash values of the topics
because we're calling the inspect
method in order to print out all of the values of the database query.
So let's clean this up by iterating over the values in the view:
<!-- app/views/static/home.html.erb --> <% @topics.each do |topic| %> <%= topic.title %> <% end %>
If you refresh the browser you'll see that this is looking better and if you stop the server and run the tests, they're all still passing.
Now that we have our titles let's put them in the sidebar where they're supposed to be and we'll also implement some of the other core layout styles in order to do this:
<!-- app/views/static/home.html.erb --> <div class="container"> <div class="row"> <div class="col-xs-9 pull-left"> <ul class="articles"> </ul> </div> <div class="col-xs-3 sidebar pull-right"> <ul> <% @topics.each do |topic| %> <li><%= topic.title %></li> <% end %> </ul> </div> </div> </div>
If you hit refresh you'll see that things are shaping up nicely on the homepage!
The last item will be to render the posts
on the homepage. Let's create a test for this that confirms that the most recent posts
show on the homepage:
# spec/features/static_spec.rb describe 'content' do it 'shows the most recent posts' do FactoryGirl.create(:post) FactoryGirl.create(:second_post) visit root_path expect(page).to have_content("My Great Post") expect(page).to have_content("Another Guide") end end
If you run the spec you'll see that it failed because it couldn't find the posts
, so let's implement the partial and create an instance variable in the controller to store the posts
:
# app/controllers/static_controller.rb class StaticController < ApplicationController def home @posts = Post.all.limit(25) @topics = Topic.all end end
And now update the homepage
template:
<!-- app/views/static/home.html.erb --> <div class="container"> <div class="row"> <div class="col-xs-9 pull-left"> <ul class="articles"> <%= render 'shared/posts' %> </ul> </div> <div class="col-xs-3 sidebar pull-right"> <ul> <% @topics.each do |topic| %> <li><%= topic.title %></li> <% end %> </ul> </div> </div> </div>
Now run the specs and you'll see an error we didn't expect ActionView::Template::Error: No route matches {:action=>"show", :controller=>"topics/posts", :id=>#<Post id: 33, title: "My Great Post", content: "Amazing content", user_id: 37, topic_id: 16, created_at: "2016-05-27 18:27:59", updated_at: "2016-05-27 18:27:59">, :topic_id=>nil} missing required keys: [:topic_id]
. It looks like I made a mistake in the partial, I wrongly anticipated that there would also be a specific Topic
to call for the link and I'm calling the @topic
instance variable. Not to worry though, we have access to the Topic
id
for each post in the post
itself. So let's update the partial:
<!-- app/views/shared/_posts.html.erb --> <h2><%= link_to post.title, topic_post_path(topic_id: post.topic.slug, id: post) %></h2>
Now if we run all of the specs we'll see that everything is working. Please note that because we're using friendly_id
we had to call the slug
method in order to get the proper URL, if you call id
it will break previous tests and will give some odd behavior in the application.
The homepage is looking great, one last item to add is to make the topics
in the sidebar clickable and for them to link to their respective pages to show the posts
nested underneath them. Let's create a test to make sure that each topic
is linking to the correct page:
# spec/features/static_spec.rb describe 'links' do it 'has topics that link to their pages' do topic = FactoryGirl.create(:topic) visit root_path expect(page).to have_link(topic.title, href: topic_path(topic)) end end
As expected this will fail, now let's implement the link code in the homepage template:
<!-- app/views/static/home.html.erb --> <% @topics.each do |topic| %> <li><%= link_to topic.title, topic_path(topic) %></li> <% end %>
Now if you run the specs you'll see all of our tests are passing and if you open the browser you'll see a very nice looking homepage with nice and shiny links to the Topic
pages:
The only refactor I want to do before ending this guide will be in the static_spec
, notice how we have a call to visit root_path
in each spec? That's always a good indicator that we need to move that process into a before block, I've put the full spec below, but try to do it first before looking at the code.
# spec/features/static_spec.rb require 'rails_helper' describe 'homepage' do before do @topic = FactoryGirl.create(:topic) FactoryGirl.create(:second_topic) FactoryGirl.create(:post) FactoryGirl.create(:second_post) visit root_path end describe 'navigate' do it 'can be reached successfully' do expect(page.status_code).to eq(200) end end describe 'content' do it 'has a list of topics' do expect(page).to have_content("Sports") expect(page).to have_content("Coding") end it 'shows the most recent posts' do expect(page).to have_content("My Great Post") expect(page).to have_content("Another Guide") end end describe 'links' do it 'has topics that link to their pages' do expect(page).to have_link(@topic.title, href: topic_path(@topic)) end end end
Running the specs will now show that everything is still passing. Notice how we had to have the create
calls before the visit
method since those posts
and topics
had to be created prior to visiting the page or the tests would not have passed. This isn't a perfect test suite since we're creating a few items that we're not using in each block, but I'd prefer to err on the side of overkill at this stage and we can always come back to the spec and clean them up to improve the speed of the test suite later on.
Now that we have a great looking homepage, topic, and post views, the next step will be to implement a design for the Topic
index
page.