How to Build an Unobtrusive Login System in Rails
An unobtrusive login system is one that gets out of the user’s way. It will make your application nicer and more polished. This article will guide you through the process of setting up user logins, then ajaxifying the process by moving the form into a modal box that communicates with the server. Additionally, this article will show you how to create the setup using jQuery and Prototype so you can choose your favorite library. This article assumes that you have experience with Rails and jQuery or Prototype.
You can use Adman65/nettuts for a successful login. Be sure to use bad credentials so you can see how everything works.
What We’re Making
Getting Started
We’re going to start by creating a dummy application that has a public and private page. The root url is the public page. There’s a login link on the public page. If the user logs in successfully, they’re redirected to the private page. If not, they’re redirected back to the login form. The private page shows the user name. We’ll use this as the starting point for ajaxifying the site.
The first step is using the rails command to generate a new application, then install and setup up authlogic.
$ cd into-a-directory
$ rails unobtrusive-login
Add authlogic.
# /config/environment.rb
config.gem 'authlogic'
Now install the gems.
$ sudo gem install gemcutter
$ sudo gem tumble
$ sudo rake gems:install
Next create a user model and add the required authlogic columns to the migration.
$ ./script/generate model User
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/user.rb
create test/unit/user_test.rb
create test/fixtures/users.yml
create db/migrate
create db/migrate/20100102082657_create_users.rb
Now, add the columns to the new migration.
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :login, :null => false
t.string :crypted_password, :null => false
t.string :password_salt, :null => false
t.string :persistence_token, :null => false
t.timestamps
end
end
def self.down
drop_table :users
end
end
Migrate the database.
$ rake db:migrate
Include authlogic in the user model.
# /app/models/user.rb
class User < ActiveRecord::Base
acts_as_authentic
end
Now we can create a user. Since this is a demo app, web based functionality for signing up isn’t required. So open up the console and create a user:
$ ./script/console
>> me = User.create(:login => 'Adman65', :password => 'nettuts', :password_confirmation => 'nettuts')
Now we have a user in the system, but we have no way to login or logout. We need to create the models, controllers, and views for this. Authlogic has its own class for tracking logins. We can use the generator for that:
# create the user session
$ ./script/generate UserSession
Next we need to generate the controller that will login/logout users. You can create sessions just like any other resource in Rails.
# create the session controller
$ ./script/generate controller UserSessions
Now set its contents to:
# /app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
def new
@user_session = UserSession.new
end
def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
flash[:notice] = "Login successful!"
redirect_back_or_default user_path
else
render :action => :new
end
end
end
It looks exactly the same as a controller that was generated via scaffolding. Now create the users controller which has public and private content. Generate a users controller. Inside the controller we’ll use a before filter to limit access to the private areas. The index action is public and show is private.
# create the users controller
$ ./script/generate controller users
Update its contents:
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
before_filter :login_required, nly => :show
def index
end
def show
@user = current_user
end
private
def login_required
unless current_user
flash[:error] = 'You must be logged in to view this page.'
redirect_to new_user_session_path
end
end
end
You should notice that current_user is an undefined method at this point. Define these methods in ApplicationController. Open up application_controller.rb and update its contents:
# application controller
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
protect_from_forgery # See ActionController::RequestForgeryProtection for details
# From authlogic
filter_parameter_logging :password, :password_confirmation
helper_method :current_user_session, :current_user
private
def current_user_session
@current_user_session ||= UserSession.find
end
def current_user
@current_user ||= current_user_session && current_user_session.user
end
end
At this point the models and controllers are complete, but views aren’t. We need to create views for a login form and the public and private content. We’ll use the nifty-generators gem to create a basic layout.
$ sudo gem install nifty-generators
$ ./script/generate nifty_layout
Time to create the login form. We’re going to use a partial for this because in the future we’ll use the partial to render just the login form in the modal box. Here’s the code to create the login form. It’s exactly the same as if you were creating a blog post or any other model.
# create the login views
# /app/views/user_sessions/_form.html.erb
<% form_for(@user_session, :url => user_session_path) do |form| %>
<%= form.error_messages %>
<p>
<%= form.label :login %>
<%= form.text_field :login %>
</p>
<p>
<%= form.label :password %>
<%= form.password_field :password %>
</p>
<%= form.submit 'Login' %>
<% end %>
Render the partial in the new view:
# /app/views/user_sessions/new.html.erb
<% title 'Login Please' %>
<%= render :partial => 'form' %>
Create some basic pages for the public and private content. The index action shows public content and show displays private content.
# create the dummy public page
# /app/views/users/index.html.erb
<% title 'Unobtrusive Login' %>
<p>Public Facing Content</p>
<%= link_to 'Login', new_user_session_path %>
And for the private page:
# create the dummy private page
# /app/views/users/show.html.erb
<% title 'Welcome' %>
<h2>Hello <%=h @user.login %></h2>
<%= link_to 'Logout', user_session_path, :method => :delete %>
Delete the file /public/index.html and start the server. You can now log in and logout of the application.
$ ./script/server
Here are some screenshots of the demo application. The first one is the public page.
Now the login form
And the private page
And finally, access denied when you try to visit http://localhost:3000/user
The AJAX Login Process
Before continuing, we need to understand how the server and browser are going to work together to complete this process. We know that we’ll need to use some JavaScript for the modal box and the server to validate logins. Let’s be clear on how this is going to work. The user clicks the login link, then a modal box appears with the login form. The user fills in the form and is either redirected to the private page, or the modal box is refreshed with a new login form. The next question is how do you refresh the modal box or tell the browser what to do after the user submits the form? Rails has respond_to blocks. With respond_to, you can tell the controller to render different content if the user requested XML, HTML, JavaScript, YAML etc. So when the user submits the form, the server can return some JavaScript to execute in the browser. We’ll use this render a new form or a redirect. Before diving any deeper, let’s go over the process in order.
- User goes to the public page
- User clicks the login link
- Modal box appears
- User fills in the form
- Form is submitted to the server
- Server returns JavaScript for execution
- Browser executes the JavaScript which either redirects or updates the modal box.
That’s the high level. Here’s the low level implementation.
- User visits the public page
- The public page has some JavaScript that runs when the DOM is ready that attaches JavaScript to the login link. That javscript does an XMLHTTPRequest (XHR from now on) to the server for some JavaScript. The JavaScript sets the modal box’s content to the form HTML. The JavaScript also does something very important. It binds the form’s submit action to an XHR with POST data to the form’s action. This allows the user to keep filling the login form in inside the modal box.
- Modal box now has the form and required JavaScript
- User clicks ‘Login’
- The submit() function is called which does a POST XHR to the form’s action with its data.
- Server either generates the JavaScript for the form or the redirect
- Browser receives the JavaScript and executes it. The browser will either update the modal box, or redirect the user through window.location.
Taking a Peak at the AJAX Ready Controller
Let’s take a look at the new structure for the UserSessions controller.
class UserSessionsController < ApplicationController
layout :choose_layout
def new
@user_session = UserSession.new
end
def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
respond_to do |wants|
wants.html { redirect_to user_path(@user_session.user) }
wants.js { render :action => :redirect } # JavaScript to do the redirect
end
else
respond_to do |wants|
wants.html { render :new }
wants.js # defaults to create.js.erb
end
end
end
private
def choose_layout
(request.xhr?) ? nil : 'application'
end
end
As you can see the structure is different. Inside the if save, else conditional, respond_to is used to render the correct content. want.xx where xx is a content type. By default Prototype and jQuery request text/JavaScript. This corresponds to wants.js. We’re about ready to get started on the AJAX part. We won’t use any plugins except ones for modal boxes. We’ll use Facebox for jQuery and ModalBox for Prototype.
Prototype
Rails has built in support for Prototype. The Rail’s JavaScript helpers are Ruby functions that generate JavaScript that use Prototype. This technique is known as RJS (Ruby JavaScript). One example is remote_form_for which works like the standard for_for adds some JS bound to onsubmit that submits to the form to its action using its method with its data. I won’t use RJS in this article since I want to demonstrate vanilla JS. I think by using pure JS and eliminating the JS helpers the article will be more approachable by less experienced developers. That being said, you could easily accomplish these steps using RJS/Prototype helpers if you choose.
Adding Prototype to the application is very easy. When you use the rails command, it creates the Prototype and scriptaculous files in /public/JavaScripts. Including them is easy. Open up /app/views/layouts/application.erb and add this line inside the head tag:
<%= JavaScript_include_tag :defaults %>
JavaScript_include_tag creates script tags for default files in /public/JavaScripts, most importantly prototype.js, effects.js, and application.js. effects.js is scriptaculous. application.js is a file you can use to keep application specific JS. Now we need a modal box plugin. We’re going to use this. Its a very nice modal box plugin inspired by OSX. The source is hosted on GitHub, so you’ll have to clone and move the files in your project directory. For example:
$ cd code
$ git clone git://github.com/okonet/modalbox.git
$ cd modalbox
# move the files in the correct directories.
# move modalbox.css into /public/stylesheets
# move modalbox.js into /public/JavaScripts
# move spinner.gif into /public/images
Now include the stylesheets and JavaScript in your application.
<%= stylesheet_link_tag ‘application’ %>
<%= stylesheet_link_tag ‘modalbox’ %>
<%= JavaScript_include_tag :defaults %>
<%= JavaScript_include_tag ‘modalbox’%>
Now let’s get our login link to open a modalbox. In order to do this we need to add some JavaScript that runs when the DOM is ready that attaches the modalbox to our link. When the user clicks the login link, the browser will do a GET to /user_sessions/new which contains the login form. The login link uses the #login-link selector. Update the login link to use the new id in /app/views/users/index.html.erb. Modify the link_to function like this:
<%= link_to 'Login', new_user_session_path, :id => 'login-link' %>
That gives us a#login-link. Now for the JavaScript to attach a modalbox. Add this JS in /public/JavaScripts/application.js
document.observe('dom:loaded', function() {
$('login-link').observe('click', function(event) {
event.stop();
Modalbox.show(this.href,
{title: 'Login',
width: 500}
);
});
})
There’s some simple JS for when the user clicks the link a modal box opens up with the link’s href. Refer to the modalbox documentation if you’d like more customization. Here’s a screenshot:
Notice that inside the modal box looks very similar to our standard page. Rails is using our application layout for all HTML responses. Since our XHR’s want HTML fragments, it make sense to render without layouts. Refer back to the example controller. I introduced a method for determining the layout. Add that to UserSessionsController to disable layout for XHR’s.
class UserSessionsController < ApplicationController
layout :choose_layout
def new
@user_session = UserSession.new
end
def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
flash[:notice] = "Login successful!"
redirect_to user_path
else
render :action => :new
end
end
def destroy
current_user_session.destroy
flash[:notice] = "Logout successful!"
redirect_to root_path
end
private
def choose_layout
(request.xhr?) ? nil : 'application'
end
end
Refresh the page and click the link you should get something like this:
Fill in the form and see what happens. If you fill in the from with bad info, you’re redirected outside the modal box. If you login correctly you’re redirected normally. According the requirements the user should be able to fill out the form over and over again inside the modal box until they login correctly. How can we accomplish this? As described before we need to use AJAX to submit data to the server, then use JavaScript to update the modal box with the form or do a redirection. We know that the modalbox does a GET for HTML. After displaying the initial modalbox, we need to write JS that makes the form submits itself AJAX style. This allows the form to submit itself inside the modal box. Simply adding this code after the modal box is called won’t work because the XHR might not have finished. We need to use Modalbox’s afterLoad callback. Here’s the new code:
document.observe('dom:loaded', function() {
$('login-link').observe('click', function(event) {
event.stop();
Modalbox.show(this.href,
{title: 'Login',
width: 500,
afterLoad: function() {
$('new_user_session').observe('submit', function(event) {
event.stop();
this.request();
})
}}
);
});
})
Form#request is a convenience method for serializing and submitting the form via an Ajax.Request to the URL of the form’s action attribute—which is exactly what we want. Now you can fill in the form inside the modal without it closing. The client side is now complete. What about the server side? The client is submitting a POST wanting JS back. The server needs to decide to either return JavaScript to update the form or render a redirect. In the UserSessionsController we’ll use respond_to to handle the JS request and a conditional to return the correct JS. Let’s begin by handling the failed login case. The server needs to return JS that updates the form, and tells the new form to submit over ajax. We’ll place this template in /app/views/users_sessions/create.js.erb. Here’s the structure for the new create action:
def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
flash[:notice] = "Login successful!"
redirect_to user_path
else
respond_to do |wants|
wants.html { render :new }
wants.js # create.js.erb
end
end
end
Now let’s fill in create.js.erb:
$('MB_content').update("<%= escape_JavaScript(render :partial => 'form') %>");
Modalbox.resizeToContent();
$('new_user_session').observe('submit', function(event) {
event.stop();
this.request();
});
First we update the content to include the new form. Then we resize the modal box. Next we ajaxify the form just as before. Voilla, you can fill in the form as many times as you want.
Next we need to handle the redirection case. Create a new file in /app/views/users_sessions/redirect.js.erb:
window.location=”<%= user_path %>”;
Now, update the create action to handle the redirection process:
def create
@user_session = UserSession.new(params[:user_session])
if @user_session.save
respond_to do |wants|
wants.html do
flash[:notice] = "Login successful!"
redirect_to user_path
end
wants.js { render :redirect }
end
else
respond_to do |wants|
wants.html { render :new }
wants.js # create.js.erb
end
end
end
And that’s it! Now try login with correct credentials and you’re redirected to the private page. For further learning, try to add a spinner and notification telling the user the form is submitting or they’re being redirect. The application still works if the user has JavaScript disabled too.
jQuery
Since I’ve already covered the Prototype process, so I won’t go into the same detail as before. Instead, I will move quickly describing the alternate JavaScript to add to the application. The jQuery vesion will have the exact same structure as the Prototype version. All we need to change is what’s in application.js, create.js.erb, and the JavaScript/css includes.
First thing we need to do is download jQuery and Facebox. Move jQuery into /public/JavaScripts as jquery.js. For facebox move the images into /public/images/, stylesheets into /public/stylesheets, and finally the JS into /public/JavaScripts. Now update /app/views/layouts/application.html.erb to reflect the changes:
<head>
<title><%= h(yield(:title) || "Untitled") %></title>
<%= stylesheet_link_tag 'facebox' %>
<%= stylesheet_link_tag 'application' %>
<%= JavaScript_include_tag 'jquery' %>
<%= JavaScript_include_tag 'facebox' %>
<%= JavaScript_include_tag 'application' %>
</head>
Facebox comes with a default stylesheet which assumes you have your images in /facebox. You’ll need to update these selectors in facebox.css like so:
#facebox .b {
background:url(/images/b.png);
}
#facebox .tl {
background:url(/images/tl.png);
}
#facebox .tr {
background:url(/images/tr.png);
}
#facebox .bl {
background:url(/images/bl.png);
}
#facebox .br {
background:url(/images/br.png);
}
Now we attach facebox to the login link. Open up /public/JavaScripts/application.js and use this:
$(document).ready(function() {
$('#login-link').facebox({
loadingImage : '/images/loading.gif',
closeImage : '/images/closelabel.gif',
});
});
I override the default settings for the images to reflect the new image path. Start the sever and head over to the index page. You should see a nice facebox with the login form:
Next thing we have to do is set the form to submit itself via AJAX. Just like before, we’ll have to use callbacks to execute code after the modal box is ready. We’ll use jQuery’s post method for the XHR request. Facebox has an after reveal hook we can use. application.js:
$(document).ready(function() {
$('#login-link').facebox({
loadingImage : '/images/loading.gif',
closeImage : '/images/closelabel.gif',
});
$(document).bind('reveal.facebox', function() {
$('#new_user_session').submit(function() {
$.post(this.action, $(this).serialize(), null, "script");
return false;
});
});
});
Updating create.js.erb should be easy enough. We have to update the facebox’s contents and re-ajaxify the form. Here’s the code:
$('#facebox .content').html("<%= escape_JavaScript(render :partial => 'form') %>");
$('#new_user_session').submit(function() {
$.post(this.action, $(this).serialize(), null, "script");
return false;
});
And that’s it! Here’s the final product:
Downloading the Code
You can get the code here. There are branches for each library so you can check out the Prototype or jQuery versions. Any questions, comments, concerns? Thanks again for reading!