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!