no content for you!

Jared Carroll

Recently we had a feature request in an app: ‘show the login box on every page if you’re not logged in except the sign-up/registration page’.

Since the login box was going to be on just about every page we decided to put it in our application wide layout:

In app/views/layouts/application.rhtml:

<div id="wrapper">

  <div id="sidebar">
    <%= render :partial => 'login_box' -%>
  </div> <!-- end #sidebar -->

  <!-- etc... -->

</div> <!-- end #wrapper -->

We also decided to put it in a partial in app/views/layouts:

In app/views/layouts/_login_box.rhtml:

<% form_tag login_url do -%>

    <label for="email">Email</label>
    <%= text_field_tag :email %>

    <label for="password">Password</label>
    <%= password_field_tag :password %>

    <%= submit_tag 'Login' %>

<% end -%>

In this application, user sign-up/registration took place at another site so adding the ability for a user to sign-up/register in this app was a new feature.

Here’s the first attempt at keeping the login box out of the sign-up/registration page (user sign-up/registration was happening in UsersController#new and UsersController#create):

In app/views/layouts/_login_box.rhtml:

<% if ! (controller.controller_name == 'users' &&
          controller.action_name == 'new') ||
        (controller.controller_name == 'users' &&
          controller.action_name == 'create')
  <% form_tag login_url do -%>
      <label for="email">Email</label>
      <%= text_field_tag :email %>
      <label for="password">Password</label>
      <%= password_field_tag :password %>
      <%= submit_tag 'Login' %>
  <% end -%>
<% end -%>

That’s terrible.

First off, any use of the controller and/or action name in a view is not allowed. If you’re doing it, you haven’t found the right design.

Let’s go back to the application wide layout:

Inapp/views/layouts/application.rhtml:

<div id="wrapper">

  <div id="sidebar">
    <% content_for :login_box do -%>
      <%= render :partial => 'login_box' -%>
    <% end %>

    <%= yield :login_box %>
  </div> <!-- end #sidebar -->

  <!-- etc... -->

</div> <!-- end #wrapper -->

Here I modified it to use #contentfor to provide some default HTML for the :loginbox symbol that’s outputted by the call to #yield. Now if I can just override that on my user sign-up/registration page I can keep the login box off those pages.

Here we go:

In app/views/users/new.rhtml:

<% content_for :login_box do -%>
<% end -%>

<!-- user sign-up/registration form... -->

Nope, didn’t work.

In Rails we know views are rendered before their corresponding layout. Now what happens when using #content_for is that each #content_for for a specific symbol simply appends to the output of the previous #content_for call for that same symbol. So what the above code results in is an empty space and then the login box HTML appearing on users/new.rhtml.

How about some conditional logic in our application wide layout?

In app/views/layouts/application.rhtml:

<div id="wrapper">

  <div id="sidebar">
    <%= yield(:login_box) || render(:partial => 'login_box') %>
  </div> <!-- end #sidebar -->

  <!-- etc... -->

</div> <!-- end #wrapper -->

Now it works.

So if we get something when #yield‘ing the :login_box symbol our login box partial won’t be rendered. And if we get nothing our login box will be rendered. Just what we wanted.

Back to our sign-up/registration form view.

In app/views/users/new.rhtml:

<% content_for :login_box do -%>
<% end -%>

<!-- user sign-up/registration form... -->

If I saw that in a view I’d probabaly rip it out. I mean why is there an empty #content_for block?

Let’s wrap it up in a helper method. I’ll throw it in application_helper.rb because that’s where it belongs.

In app/helpers/application_helper.rb:

def no_content_for(symbol)
  content_for(symbol) { '' }
end

The block that returns an empty String hack is there because if you don’t return anything you get a error from Rails complaining about calling #+ on nil. Apparently, an empty #content_for block in a view must return at least an empty String.

And our final view:

In app/views/users/new.rhtml:

<% no_content_for :login_box -%>
<!-- user sign-up/registration form... -->

Next!

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.