Updating the Navigation View when a User is Signed In
In this guide we will implement the functionality to dynamically update the view based on if a user is logged in or not.
Guide Tasks
  • Read Tutorial

In this guide we will implement the functionality to dynamically update the view based on if a user is logged in or not. For small applications and tutorials it's pretty common to accomplish this type of behavior by leveraging the current_user method provided by the Devise gem, and that's fine for beginner applications (I even used it in the Learn Ruby on Rails from Scratch course), however this implementation has a few issues. First let's see what this could look like in our app:

<!-- app/views/shared/_header.html.erb -->

<% if current_user %>
  <strong>Welcome <%= current_user.full_name %></strong>
  <%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% else %>
  <strong>Welcome Guest!</strong>
  <%= link_to "Login", new_user_session_path %> or
  <%= link_to "Register", new_user_registration_path %>
<% end %>

This isn't horrible, however we have some duplicate code and more importantly it's not a very confident way to code. We're essentially saying:

We're not sure if a user is going to be signed in or not, so we need to check it in the view so we don't get a nil error.

It's a bad practice to check for nil values in the view. A better implementation would be to override the current_user method so that we're 100% sure that it won't return nil, instead we'll have it return a Guest object. For applications that utilize a robust permission structure we may even want to implement a GuestUser class, however for our app we can create an OpenStruct that will have all of the necessary parameters that a user would have so that even if we run code such as: current_user.full_name, when a user isn't logged in it will output something like Guest.

Let's start off by creating some specs that check for this behavior. Inside of the static_spec we'll create a new describe block that will test the header partial. Technically you could put this in a view test, however since this is going to combine implementation changes to multiple parts of the application I like designating it as an integration spec (I'm also note a huge fan of view tests since they seem to break too easily, especially at this stage of the application).

# spec/features/static_spec.rb

  describe 'header' do
    it 'has a header that displays the users name' do
      user = FactoryGirl.create(:user)
      login_as(user, :scope => :user)
      visit root_path

      expect(page).to have_content("Jon Snow")
    end
  end

This fails and let's us know that it couldn't find the user's name:

large

Let's implement the most basic implementation to get this working, we'll simply paste in the view code referenced at the beginning of this guide:

large

If you run the tests now you'll see that this is working. We're going to save our refactor step until after our next spec. Let's now add in a spec that checks for what happens when a guest user accesses the page:

# spec/features/static_spec.rb

  describe 'header' do
    it 'displays a welcome message to guest users' do
      expect(page).to have_content("Guest")
    end
  end

If you run the specs now you'll see that we didn't get a failure (mainly because I skipped a little ahead and put the current_user check in the view). So now let's refactor this code so that we don't have a nil check in the view. Let's start by updating our ApplicationController code:

# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  def current_user
    super || OpenStruct.new(full_name: 'Guest')
  end
end

I hope you paid attention in the Ruby programming course because this is going to leverage that knowledge in a few areas. Let's walk through what we're doing here:

  1. We're overriding the Devise current_user method, since the ApplicationController is called after the Devise gem loads our new method will override the gem method.

  2. By calling super first, this means that we're telling Rails to simply use the Devise version of current_user.

  3. If no user is logged in, in other words if current_user is nil the pipes || tell the method to instead return an OpenStruct object that has one attribute: full_name and it's set to "Guest".

This is much better behavior for the application to have, now we won't have to worry about our current_user calls returning nil and causing errors. Side note: this is a good pattern to follow for many other classes as well.

Now let's refactor our view code:

<!-- app/views/shared/_header.html.erb -->

<strong>Welcome <%= current_user.full_name %></strong>

<% if current_user.is_a?(OpenStruct) %>
  <%= link_to "Login", new_user_session_path %> or
  <%= link_to "Register", new_user_registration_path %>
<% else %>
  <%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% end %>

So here we've moved our current_user.full_name out of a conditional since we now know it won't be nil. I'm keeping the login/register/logout links in a conditional and we may come back and refactor this later on, but for right now I'm happy with how it's looking now. If you run rspec now you'll see that all of the tests are passing and we're back to green. You can also test it out in the browser and see that this works when a user is logged in:

large

And when a guest user is on the site:

large

An added benefit to this implementation is that this doesn't only help out our header partial, by setting an OpenStruct our current_user method now is guaranteed to work, even if another attribute is called. If you sign out and place the code <%= current_user.email %> anywhere in your view and refresh, you'll see that the app doesn't break! Notice that we don't have an email attribute in the OpenStruct, however since the object exists the page will load normally.

Now that our view is reflecting the user login status, in the next lesson we're going to start implementing the permission structure, starting with installing the Pundit gem.

Resources