Thursday, December 15, 2011

GWT History Mechanism

GWT provides a way to let you interact with the history of the client's browser. This helps to better integrate your GWT application with the browser and provide a more streamlined experience to the user. It allows the application to respond to the user clicking the "back" and "forward" buttons.

In this tutorial, I'm going to add history support to my TwitterSearch application that I created in some previous blog posts. This is a simple app that allows the user to perform tweet searches, as well as view the Twitter privacy policy. By adding history support, the user will be able to navigate back to searches that she made previously. Feel free to download the complete source code so that you can better follow along with this blog post.

What the TwitterSearch application looks like

Because GWT apps never navigate away from the main, HTML page, the way GWT implements history is by adding a fragment identifier to the URL. The fragment identifier starts with a hash (#) and comes at the end of the URL. You often see fragment identifiers being used on large webpages. It allows you to jump around to different parts of the same page by clicking links that have fragment identifiers in their URLs. GWT, however, uses them to identify the "state" that the application was in at a specific point in history.

Enable history support

The first thing to do is make sure your GWT application has history support enabled. This is done by adding an <iframe> element to the HTML page (if you've created your GWT application using Eclipse, then it will be enabled by default):

<html>
  [...]
  <body>
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
    [...]
  </body>
</html>

Adding history tokens

Next, we must figure out when to add entries to the browser's history. In our case, we want to add an entry when the user performs a search and when the user views the privacy policy.

searchButton = new Button("Search");
searchButton.addClickHandler(new ClickHandler() {
  @Override
  public void onClick(ClickEvent event) {
    String query = searchQueryTextBox.getText();
    History.newItem(query);
    doSearch(query);
  }
});

privacyPolicyButton = new Button("Privacy Policy");
privacyPolicyButton.addClickHandler(new ClickHandler() {
  @Override
  public void onClick(ClickEvent event) {
    History.newItem("_privacyPolicy");
    doGetPrivacyPolicy();
  }
});

Here, we're adding a new history token when the "Search" and "Privacy Policy" buttons are clicked by calling the static History.newItem() method (new instances of the History class are never created--only its static methods are used). This method simply takes a String as an argument, which is the text that will appear in the fragment identifier. We use the search query as this string for tweet searches, and we use the hard-coded string _privacyPolicy for when the privacy policy is viewed. The doSearch() and doGetPrivacyPolicy() methods call the Twitter API and then update the UI with the results (download the complete TwitterSearch project to see how this is done, or read my previous blog posts).

Try performing a search now--you'll see the search query appear in the fragment identifier of the URL. But if you click "back", nothing will happen! We must explicitly tell our application how to respond to these events.

The history token is added to the fragment identifier in the URL

Handling history change events

Now, let's handle what happens when the user clicks the "back" or "forward" buttons. We'll do this in our onModuleLoad() method, so that the handler we define is registered as soon as our app loads.

@Override
public void onModuleLoad() {
  History.addValueChangeHandler(new ValueChangeHandler<String>() {
    @Override
    public void onValueChange(ValueChangeEvent<String> event) {
      String token = event.getValue();
      if ("_privacyPolicy".equals(token)){
        doGetPrivacyPolicy();
      } else {
        searchQueryTextBox.setText(token);
        doSearch(token);
      }
    }
  });

  [...]
}

As you can see, we call the History.addValueChangeHandler() method and pass in an implementation of the ValueChangeHandler interface. This handler will be invoked every time the user goes back or forward in their browser history. In the handler's only method, onValueChange(), we get the history token (which is the fragment identifier in the URL), and then use it to update our UI accordingly.

Handling history tokens on app startup

Besides giving the user the ability to use "back" and "forward", another good thing about using history is that the user can save bookmarks of our app. For example, a user might want to frequently check Twitter for news on her favorite celebrity, Brad Pitt. She might want to bookmark her search, so that she can reopen the bookmark at a later time and immediately see the latest search results.

To do this, we need to check for an existing history token on startup. We must check for it manually because the presence of a history token on startup will not fire a ValueChangeEvent (this event is only fired when the user clicks "back" or "forward").

@Override
public void onModuleLoad() {
  [...]

  String token = History.getToken();
  if (!token.isEmpty()){
    if ("_privacyPolicy".equals(token)){
      doGetPrivacyPolicy();
    } else {
      searchQueryTextBox.setText(token);
      doSearch(token);
    }
  }
}

The History.getToken() is called to get the current history token. The method returns an empty string if there is no token, so we first check to see if the string empty. If it's not, then we update the UI according to the token. Note that this should happen at the very end of the onModuleLoad() method because the UI widgets must be created first in order for the UI to be updated.

Does this code look familiar? It should because much of it is identical to the code we wrote for the History.addValueChangeHandler() method, so let's refactor it out into its own method:

@Override
public void onModuleLoad() {
  History.addValueChangeHandler(new ValueChangeHandler<String>() {
    @Override
    public void onValueChange(ValueChangeEvent<String> event) {
      String token = event.getValue();
      handleHistoryToken(token);
    }
  });

  [...]

  String token = History.getToken();
  if (!token.isEmpty()){
    handleHistoryToken(token);
  }
}

private void handleHistoryToken(String token){
  if ("_privacyPolicy".equals(token)){
    doGetPrivacyPolicy();
  } else {
    searchQueryTextBox.setText(token);
    doSearch(token);
  }
}

Changing the window title

It's also helpful to change the window title of the web page to reflect the history token. That way, when the user views their entire history, they can have a better idea of what Twitter searches they performed at a glance. To do this, we'll modify two sections of code. First, we'll modify the place where we add new history tokens.

searchButton = new Button("Search");
searchButton.addClickHandler(new ClickHandler() {
  @Override
  public void onClick(ClickEvent event) {
    String query = searchQueryTextBox.getText();
    History.newItem(query);
    Window.setTitle("Twitter Search - " + query);
    doSearch(query);
  }
});

privacyPolicyButton = new Button("Privacy Policy");
privacyPolicyButton.addClickHandler(new ClickHandler() {
  @Override
  public void onClick(ClickEvent event) {
    History.newItem("_privacyPolicy");
    Window.setTitle("Twitter Search - Privacy Policy");
    doGetPrivacyPolicy();
  }
});

Here, we've added calls to Window.setTitle() to set the title of the browser window. It's very important that you call this method after adding the history entry. Otherwise, the titles will not be synced properly with the history entries.

Second, we'll modify our handleHistoryToken() method to set the proper Window title when a history token is loaded.

private void handleHistoryToken(String token){
  if ("_privacyPolicy".equals(token)){
    Window.setTitle("Twitter Search - Privacy Policy");
    doGetPrivacyPolicy();
  } else {
    searchQueryTextBox.setText(token);
    Window.setTitle("Twitter Search - " + token);
    doSearch(token);
  }
}

Now, when you view your history, you'll get a good glimpse of all your past searches.

What the browser history looks like before and after setting window titles.

I hope you've enjoyed my GWT History tutorial. For more information, please see the GWT History page in the GWT Developer's Guide.

2 comments:

PhiLho said...

This is very useful, particularly the part about changing the window title, because having the capability to navigate the history, but having all pages with the same name reduces the interest of the operation... :-)
Particularly on modern browser able to show the recent history with a mouse down on the back / forward buttons.

One thing I would have loved to see is how this window title can be set in the context of Activity & Places, which relies on the GWT history, but hides details.

Michael Angstadt said...

Thanks PhiLho, I'm glad you found my blog post helpful. Unfortunately, I don't know much about Activity & Places.