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!