GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

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="sidebar">
  <%= render :partial => 'login_box' -%>
</div> <!-- end #sidebar -->

<!-- etc... -->

<!– end #wrapper –>

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

app/views/layouts/loginbox.rhtml

<% formtag loginurl do -%>

<%= textfieldtag :email %>

<%= passwordfieldtag :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):

app/views/layouts/loginbox.rhtml

<% if ! (controller.controllername == 'users' && controller.actionname == 'new') || (controller.controllername == 'users' && controller.actionname == 'create')

<% formtag loginurl do -%>

<%= textfieldtag :email %>

<%= passwordfieldtag :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:

app/views/layouts/application.rhtml

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

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

<!-- etc... -->

<!– 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:

app/views/users/new.rhtml

<% contentfor :loginbox 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 #contentfor is that each #contentfor 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="sidebar">
  <%= yield(:login_box) || render(:partial => 'login_box') %>
</div> <!-- end #sidebar -->

<!-- etc... -->

<!– 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

<% contentfor :loginbox 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 nocontentfor(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

  <% nocontentfor :login_box -%>

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

Next!