giant robots smashing into other giant robots

We are thoughtbot, a web design and development agency in Boston, MA.

Comments (View)

no content for you!

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:

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’:

app/views/layouts/_login_box.rhtml

  <% form_tag login_url do -%>
    <p>
      <label for="email">Email</label>
      <%= text_field_tag :email %>
    </p>
    <p>
      <label for="password">Password</label>
      <%= password_field_tag :password %>
    </p>
    <p>
      <%= submit_tag 'Login' %>
    </p>
  <% 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):

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 -%>
    <p>
      <label for="email">Email</label>
      <%= text_field_tag :email %>
    </p>
    <p>
      <label for="password">Password</label>
      <%= password_field_tag :password %>
    </p>
    <p>
      <%= submit_tag 'Login' %>
    </p>
  <% 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:

app/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 #content_for to provide some default HTML for the :login_box 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:

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?

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.

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.

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:

app/views/users/new.rhtml
  <% no_content_for :login_box -%>

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

Next!