Unobtrusive Javascript based login window

Login window which appears on top of the content.

Login functionality is one of the gates to the members area. It should be provided in very accessible and clear form. I think that the best solution is to display a form on top of existing page, send the login request with automatically forward the action to the desired one.

The example uses Prototype and PHP, but it shouldn't be complicated to rewrite it for other software.

I've used this functionality on William Baker's B*Boy website which I had honour to build with Hatch recently.

I will show the details of the frontend part. In the backend I will refer to the magic User class which will contain a User::signin($lo­gin,$password) method. To simplify the code login div and form will be in DOM all the time (just hidden).

Login Form

This login form could be created by JavaScript to obtain full „unobtrusiveness“, if you want me to write the code, I'll do it later. This should be clearer.

<div id='generic_login_div' class='hidden'>
  <form id='generic_login_form' action='/login' action='post'>
    <h3>Login</h3>
    <p class='form-row'>
      <label for='username'>username:</label>
      <input type='text' name='username' id='username'/>
    </p>
    <p class='form-row'>
      <label for='password'>password:</label>
      <input type='password' name='password' id='password'/>
    </p>
    <p class='submit-row'>
      <input type='submit' value='login'/>
      <a onclick='hideLogin()'>Cancel</a>
    </p>
  </form>
</div>

One has to have a similar login form displayed on /login. It will be displayed for non Javascript users. It may be exactly the same code (just remove class hidden).

It's necessary to hide the div element using CSS.

/* Hide all elements contains class "hidden" */
.hidden { display: none; }
/* Style the div */
#generic_login_div {
  /* Place the login div on top of the page */
  z-index: 101;
  /* Define the position (i.e. center top) */
  position: absolute;
  width: 300px;
  top: 0;
  left: 50%;
  margin-left: -150px;
  /* Some important styling */
  background: white;
  border: 1px solid black;
}

There is a need to have functions to show and hide the div. I will use the Scriptaculous, but certainly $(el).show() or hide is enough

function showLogin() {
  if ($('generic_login_div').hasClassName('hidden')) {
    $('generic_login_div').hide();
    new Effect.Appear('generic_login_div');
    $('generic_login_div').removeClassName('hidden');
  }
}
function hideLogin() {
  if (!$('generic_login_div').hasClassName('hidden')) {
    new Effect.Fade('generic_login_div',{
      afterFinish: function () {
        $('generic_login_div').addClassName('hidden');
      }
    });
    $('generic_login_div').removeClassName('hidden');
  }
}

At the moment it's enough to just call the showLogin() function form any link. The form will be send to /login. It may be done like that:

<a href='/login' onclick='showLogin();return false;'>Login</a>

The login script could be written like that:

if (User::signin($_POST['username'],$_POST['password'])) {
  // startLabel: LoinSuccess
  $_SESSION['success'] = 'Welcome again';
  header('Location: /dashboard'); // redirect to default location
  // endLabel
} else {
  $_SESSION['error'] = 'Wrong login or password';
  header('Location: /login'); // display login page with an error
}

Redirect to current page

The login functionality works. But the page which is displayed afterwards is always a dashboard. To add the redirection I'll use sessions.

Every page has to save it's location:

$_SESSION['last_location'] = '/downloads'; // save current page location in our example (/downloads)

The login script has to read it and redirect using this value

// startLabel: loginSuccess
$_SESSION['success'] = 'Welcome again';
$redirect = (array_key_exists('last_location')) ? $_SESSION['last_location'] : '/dashboard';
header('Location: ' . $redirect); // redirect to last or default location
// endLabel

Good. Most users could end at this point. There is still no AJAX, but the solution is simple and unobtrusive. To put things a little bit further I'd like to tackle a more complicated solution.

Let's imagine we have more links to restricted content on the page. One is just ‚/login‘ – after succesfull login user stays on the page, the other is ‚/restricted_dow­nload‘ which should be displayed only for logged in users. I'd like to login a user and redirect him to the restricted page if login was successful.

To do it I will create a new anchor tag:

<a href='/restricted_download' class='login redirect'>restricted download</a>

And change the login one:

<a href='/login' class='login'>login</a>

To keep the code unobtrusive the /restricted_dow­nload script needs to display the login script for guests:

// set the 'last_location'
$_SESSION['last_location'] = '/restricted_download';
if (!$user->isAuthenticated()) {
  $_SESSION['notify'] = 'You need to be logged in to access this page';
  header('Location: /login');
}

Every page needs to be parsed to check if there is any anchor tag with class login. It has to be initiated inside the code:

<script type='text/javascript'>
Event.observe('window','load',function () { initLogins(); });
</script>

To find all anchor tags with login class I will use the $$ function which is returning array of DOM elements. These elements will be changed to use onclick action rather than href.

function initLogins() {
  $$('a.login').each( function (el) {
    if (el.hasClass('redirect')) el.redirect = el.href;
    else el.redirect = '';
    el.href = '#';
    Event.observe(el,'click', function () {
      // startLabelJSRedirectA
      showLoginWithRedirect(el.redirect);
      // endLabel
    });
  });
}

There is a need to send redirect to /login:

function showLoginWithRedirect( redirect ) {
  new Insertion.After($('generic_login_form'),"<input type='hidden' name='redirect' value='"+redirect+"'/>");
  showLogin();
}

And use it in login script:

// startLabel: loginSuccess
$_SESSION['success'] = 'Welcome again';
$redirect = (array_key_exists('last_location')) ? $_SESSION['last_location'] : '/dashboard';
// Add redirect from login form
if (!empty($_POST['redirect']) $redirect = $_POST['redirect'];
header('Location: ' . $redirect); // redirect to last or default location
// endLabel

This is full solution, but it still does need to reload the page after every login. Better attempt would be to use AJAX, which will return success in JSON and other functions will use this response and take further actions.

I'll write the AJAX solution in the next couple days as a new post. Please do check soon!

Happy Easter!

Links

  1. Prototype functions $$ each

Trackback URL for this post:

http://piotr.zalewa.info/trackback/49
from unobtrusive on 2008, May 23 - 04:08

Bookmarked your post over at Blog Bookmarker.com!