Part 9: Login auto-complete
In this post we'll have a look at how to get login form auto-complete working with GWT.
The browser login auto-complete feature saves your username and password (if you choose to) so that you don't have to type it every time you want to log in. Depending on how sensitive data your application is presenting, you might want to disable this feature altogether. For example, you would probably not want auto-complete enabled for a bank application, although I would probably switch bank if their user authentication model only consisted of username/password checks. For Teamscape, we will enable both auto-complete and a "Remember me" option like Gmail has. We will look into the Remember me feature later when we have enabled user authentication and sessions.
You might wonder why we need a post about this in the first place. Can't we just put a TextBox and a PasswordTextBox in a view and call it a day? Unfortunately not, since the browser requires that the login form is present in the original HTML markup. It doesn't work if we inject the form with JavaScript, which is what GWT does. Read more about this here.
There are different ways to solve this, but I think what Thomas Broyer suggests here is the cleanest solution that integrates nicely with GWT. What we are going to do is to add an invisible div to our HTML file that contains our login form, and then we fetch the elements from the DOM by id in our LoginView.
<!-- In order for the browser auto-complete feature to work, --> <!-- we must define the login form in the original HTML markup --> <div id="loginDiv" style="display:none"> <form action="login" id="loginForm"> <table id="loginTable" valign="center"> <tr> <td id="loginLabel">Username:</td> <td><input id="loginUsername" name="u" style="margin-top:0px"></td> </tr> <tr> <td id="loginLabel">Password:</td> <td><input id="loginPassword" name="pw" type="password" style="margin:0px"></td> </tr> <tr> <td><button id="loginSubmit" type="submit">Login</button></td> <td id="loginRememberMe"></td> </tr> </table></form> </div>
I have used a table here to align the components, but you can use any style you want as long as the login form is defined correctly. We will probably use CSS here later to apply look and feel instead.
Now, let's have a look at our LoginView.
public class LoginView extends BaseView implements LoginPresenter.Display {
// Login ids. These MUST match the ids in TeamScape.html!
private static final String LOGINFORM_ID = "loginForm";
private static final String LOGINBUTTON_ID = "loginSubmit";
private static final String REMEMBERME_ID = "loginRememberMe";
private static final String USERNAME_ID = "loginUsername";
private static final String PASSWORD_ID = "loginPassword";
private final VerticalPanel mPanel;
private final EventBus mEventBus;
@Inject
public LoginView(EventBus eventBus) {
mEventBus = eventBus;
// Get a handle to the form and set its action to our jsni method
FormPanel form = FormPanel.wrap(Document.get().getElementById(LOGINFORM_ID), false);
form.setAction("javascript:__gwt_login()");
// Get the submit button for text localization
ButtonElement submit = (ButtonElement) Document.get().getElementById(LOGINBUTTON_ID);
submit.setInnerText(AppController.getConstants().login());
// We have reserved a table cell for a remember me checkbox, so let's
// add that to the cell
TableCellElement rememberMeCell = (TableCellElement) Document.get().getElementById(REMEMBERME_ID);
CheckBox box = new CheckBox(AppController.getConstants().rememberMe());
rememberMeCell.appendChild(box.getElement());
// Now, inject the jsni method for handling the form submit
injectLoginFunction(this);
// Add the form to the panel
mPanel = new VerticalPanel();
mPanel.add(form);
initWidget(mPanel);
}
// This is our JSNI method that will be called on form submit
private native void injectLoginFunction(LoginView view) /*-{
$wnd.__gwt_login = function(){
view.@se.teamscape.client.view.LoginView::doLogin()();
}
}-*/;
// This is our internal method that is called from the JSNI method
@SuppressWarnings("unused")
private void doLogin() {
mEventBus.fireEvent(new LoginRequestEvent(getUsername(), getPassword()));
}
@Override
public String getPassword() {
return ((InputElement) Document.get().getElementById(PASSWORD_ID)).getValue();
}
@Override
public String getUsername() {
return ((InputElement) Document.get().getElementById(USERNAME_ID)).getValue();
}
}
The code pretty much speaks for itself. The important thing is that we have defined a JSNI method that we set as the action for our form. The JSNI method then calls into our LoginView where we handle the login request by firing off a LoginRequestEvent. We can't do the normal "HasClickHandler" thing to handle the button click, and we might want to listen to login requests in more than one place.
That was it!
April 22nd, 2011 - 16:13
Could you e-mail me the source code. I´m very interested in implementing a Login System in my new app. martinhoarantes@sapo.pt
May 6th, 2011 - 19:36
Thanks, very nice post. Simple and clean approach.
I am using the same approach as described above and tested IE versions works fine. But in Firefox tested in 3.5 and above and it prompts and saves the data correctly but does not populate them back.
Any pointer would be highly appreciated.
May 9th, 2011 - 21:19
I resolved the above issue by changing the form action in the HTML to be the same as javascript:__gwt_login(). For some reason when we were setting a different action from the GWT Firefox did not like it.
August 31st, 2011 - 12:50
There is also another simpler solution.
In the host html page add this small piece:
In your entry point class add this snippet:
Element usernameEl = DOM.getElementById(“login_username”);
Element passwordEl = DOM.getElementById(“login_password”);
TextBox usernameBox = (usernameEl == null ? new TextBox() : TextBox.wrap(usernameEl));
PasswordTextBox passwordTextBox = (passwordEl == null ? new PasswordTextBox() : PasswordTextBox.wrap(passwordEl));
FormPanel form = new FormPanel(“”);
form.add(usernameEl);
form.add(passwordTextBox);
Adding the form to a Panel will autofill the username and password with the saved credentials!
Obviously, I omitted from the snippet all the form formatting part!
August 31st, 2011 - 12:53
Damn! The HTML part of the comment was deleted. I’ll replace the angular brackets with square one:
[form style="display: none"]
[input type="text" name="username" id="login_username" /]
[input type="password" name="password" id="login_password" /]
[/form]