What this tutorial does, is walk you through the stages of creating a project which will look like the demo below. The code you see is taken from the actual project used to create the demo. The stages in building it are the stages I went through to create it.
It's a working demo, so you can click the tabs and then use the history in your browser to navigate back and forward.
History is not as complicated as I first thought it might be, but you need to take a bit of time to get your head around it.
Luckily, for some reason, browser history doesn't just affect the top-level page
in the browser, but embedded frames as well. If you go to Panels | Frame and
navigate somewhere else and then hit 'Back' in your browser, you can see this.
Actually, you can see it in the demo, too, because that is in an IFRAME
too.
GWT changes that hidden history frame if you call the newItem() method.
When the user navigates using the browser Back and Forward buttons, GWT fires an
onHistoryChanged() event for the History class.
What you have then, is nothing more than being able to stick a string in a stack and get the string fired back at you if the user tries to move up or down the stack. You can move back and forth programmatically, you can get the token, and you can add and remove listeners, but that is it, that's all you get.
Actually, it's a very useful way of doing things, but, like sliced bread, it had to be thought of first and then, because it's so simple, you think: is that all?
If at any point I seem to be stating the bleedin' obvious, then please just skip that bit, realising that you are one of the blessed. I try to bear in mind that it's only 'bleedin' obvious' when you know about it. (Most people think a mouse is an animal - I ask you!) And if I keep repeating myself, it's just because I've got children, a dog, elderly parents, voicedial, children and a dog.
You need to create a history listener. When the user clicks the 'Back' or 'Forward' button
on their browser, the appropriate method in the listener will get run. Since there is only
one method in a history listener, then it will always be the onHistoryChanged() method.
There are a number of ways of creating a listener (see the Listeners | ClickListener page for five) but in this situation, where there is only ever going to be one history listener in this class (and project, for that matter) then the most compact and Spartan and Zen and impressive way of doing it is to make this class into its own history listener. It's also less typing.
To do this, the class needs to implement the HistoryListener interface, telling the system
that it can be used as a HistoryListener. As a HistoryListener, this
class must implement all the methods in the interface, in this case, one, the
public void onHistoryChanged(String historyToken) method. It doesn't actually have
to do anything, but it must be there. A bit like the staff at B & Q.
public class Main implements EntryPoint, HistoryListener
{
public void onModuleLoad()
{
}
public void onHistoryChanged(String historyToken)
{
}
}
Just because our class is a HistoryListener, it doesn't mean that it will get called
when history changes. The 'Powers That Be' have to be told that you want to be on the receivers'
list (this is not the Readers' Digest here, it doesn't just turn up) so we need to call the
addHistoryListener method of the History class and register. Since our
class wants to register itself, it can use the this keyword. Actually, it's not
very easy to use anything else.
public class Main implements EntryPoint, HistoryListener
{
public void onModuleLoad()
{
History.addHistoryListener(this);
}
public void onHistoryChanged(String historyToken)
{
}
}
Now, every time history changes the onHistoryChanged() will fire. The
historyToken will contain whatever you wrote to it. (OK, you haven't
done the writing bit yet, but we're coming to that - we're putting the target up
before we start firing events at it).
You can actually do whatever you like with the history token. It doesn't even have to be used for history (but you'll confuse your users if you use it for anything else). You can, as in this web site, save a chain of menu titles so you can drill down. You could, for a specific tab, hold a view and an order number and display the order in a certain way.
Here, we'll start of with a simple, one-level menu with three pages.In this simple system the pages are 'Home', 'About Me', and 'Contact Me. These are kept in a TabPanel. So we need a TabPanel and enough of a page in each so we can tell them apart. Here's the code for a basic page that gets switched to when the user clicks a tab.
FlowPanel page(String html)
{
// A holder panel - just a wrapper to avoid a sizing problem
FlowPanel panel = new FlowPanel();
// The main area for the page is just whatever HTML we pass to it
HTML page = new HTML(html);
panel.add(page);
// Set the page size and align the text centrally
page.setSize("300px", "200px");
page.setHorizontalAlignment(HasAlignment.ALIGN_CENTER);
// Add a border and some padding to give it that 'WOW' factor
DOM.setStyleAttribute(page.getElement(), "border", "10px solid blue");
DOM.setStyleAttribute(page.getElement(), "padding", "20px");
// Return the panel to whatever asked for it
return panel;
}
Next we need to add the TabPanel. We add it at the class level because we
are going to have to refer to it in the onHistoryChanged()
method.
In the onModuleLoad() method, we will need to add the pages.
In a real system, you'd be adding classes instead of strings of HTML, but
here we just want to keep it simple and see it work.
TabPanels don't start off with anything selected, so we need to do that. We'll
select tab zero (as the Home page this seems reasonable). We will also add our
little site to the RootPanel.
For a live site you would probably use onHistoryChanged(History.getToken());
which would put your site in whatever state the hisory token dictated. This
means that if a user follows a link or bookmark, they wouldn't get the Home page,
they would get the page they were after.
TabPanel menu = new TabPanel();
public void onModuleLoad()
{
menu.add(page("Welcome to my Home Page"), "Home");
menu.add(page("This page is all about Me"), "About Me");
menu.add(page("If you click <a href='mailto:ianbambury@gmail.com'>"
+ "here</a> you can email me"), "Contact Me");
menu.selectTab(0);
RootPanel.get().add(menu);
History.addHistoryListener(this);
}
OK, we have a tab panel to use as a menu, and we have some pages in them. We also have a history listener to react to history events sent to it. Trouble is, we have no history tokens for our page. The previous one is currently the page the user just came from. What we need is multiple tokens for our page - i.e our site plus a bit after the hash (#) - an internal bookmark.
For history to work we need to add tokens to the history stack so there is something to get given back when the user navigates within the web browser
Since we are going to change the tab programmatically, the easiest thing to
save is the tab index of the page as the user navigates to it, so that is
what I'll use here. In real life, you'd probably want to save the text of
the tab and look it up by running through each tab until you match it. That
way, when someone is enamoured by your web page and saves a link, they will
see http://mysite.com/#lovelypage which means more than
http://mysite.com/#42 to a human. But I'm trying to keep the
demo simple.
We need to find a place in the code to save this token. The TabPanel
has an associated listener - the aptly named TabListener.
There are two methods in this listener, onBeforeTabSelected() and
onTabSelected(). We need the second one.
In keeping with our Zen theme of simplicity (and the author's desire not to type too
much if he doesn't have to), we will turn our entry class
into a TabListener as well as a HistoryListener
public class Main implements EntryPoint, HistoryListener, TabListener
This forces us to add the two methods in the TabListener interface.
Note that: if you let Eclipse add these methods, it
will default the first to return false; which will cancel the change
of tab. When I first started using GWT, Eclipse and Java, I didn't notice this and,
of course, the damn TabPanel stopped working. I spent a while working out why. Recode
it as return true; and it will show.
So we have declared our class as a tab listener and we need to add the new methods.
In the same way as all listeners, we need to register our class in order to get sent
the events. menu.addTabListener(this); does this.
public void onModuleLoad()
{
menu.add(page("Welcome to my Home Page"), "Home");
menu.add(page("This page is all about Me"), "About Me");
menu.add(page("If you click <a "
+ "href='mailto:ianbambury&gmail.com'>here</a>"
+ " you can email me"), "Contact Me");
menu.selectTab(0);
menu.addTabListener(this);
RootPanel.get().add(menu);
History.addHistoryListener(this);
}
public boolean onBeforeTabSelected(SourcesTabEvents sender, int tabIndex)
{
return true;
}
public void onTabSelected(SourcesTabEvents sender, int tabIndex)
{
History.newItem(""+tabIndex);
}
All that is left to do is to actually process the token.
Since our method gets fired every time the history stack changes, it follows that
this method will be called when we change the history stack ourselves in the
onTabSelected() method. The TabPanel will be on the right tab (by
definition) and so we don't want this processing to run. If the TabPanel is on
the right tab, we will never want this processing to run, so we need to do a check
It's also a good place to do this as everything passes through here, you don't have
to check anywhere else, so you never have to worry about it again.
In this simple example, it doesn't really matter if the tab effectively gets clicked
twice. What would happen is the code would run a second time and (luckily) it wouldn't
loop (since the second time we tried to write a history item, the new item would be
the same as the old item and not even get written, therefore it would not have changed,
therefore the onHistoryChanged(); event wouldn't fire).
Thing is, if the page creation process is complicated, time consuming, and/or
contacts the server, you will double the time lag before the page appears, and maybe
do things twice that you only want to do once (like write to a log, for example).
So, after checking that, all we need to do is retrieve the tab index from the token, do a bit more checking to see if it is valid (the user may be effing around with the bookmark just to see what happens, or someone may be following a saved link from the old days when we had 42 tabs), and then change the tab
String oldToken = null;
public void onHistoryChanged(String historyToken)
{
// If they are the same, no need to do anything
if (oldToken != null && historyToken.equals(oldToken)) return;
// Save the token for the next time round
oldToken = historyToken;
// Get the tab index
int index = 0;
try
{
index = Integer.parseInt(historyToken);
}
catch (Exception e)
{
}
// Do a bit of checking
if (index < 0 || index >= menu.getTabBar().getTabCount()) index = 0;
menu.selectTab(index);
}
That's it - the creation of a web site complete with history support. It doesn't actually look much when you see it like this, especially if you take the comments out, but it took a while for me to learn enough to reduce it to this.
If you'd like to send in a vote from the end of this page, that would be really good. I'd like to hear that the page was worth doing. I'd like to hear if it wasn't and I was wasting my time, too. If you have any ideas for improvements or changes, or if you spot any errols or tryping mostakes, or if you have any ideas for other tutorials which you think would be useful, then please take a few moments to let me know. I'd really appreciate it. If you have any questions, then either use the comment box, or just email me - address on the home page. Thanks.
I hope, especially since you got this far, that you got something out of it.
package com.roughian.appmin.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.HistoryListener;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasAlignment;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.SourcesTabEvents;
import com.google.gwt.user.client.ui.TabListener;
import com.google.gwt.user.client.ui.TabPanel;
public class Main implements EntryPoint, HistoryListener, TabListener
{
TabPanel menu = new TabPanel();
public void onModuleLoad()
{
menu.add(page("Welcome to my Home Page"), "Home");
menu.add(page("This page is all about Me"), "About Me");
menu.add(page("If you click <a "
+ "href='mailto:ianbambury&gmail.com'>here</a>"
+ " you can email me"), "Contact Me");
menu.selectTab(0);
menu.addTabListener(this);
RootPanel.get().add(menu);
History.addHistoryListener(this);
}
String oldToken = null;
public void onHistoryChanged(String historyToken)
{
// If they are the same, no need to do anything
if (oldToken != null && historyToken.equals(oldToken)) return;
// Save the token for the next time round
oldToken = historyToken;
// Get the tab index
int index = 0;
try
{
index = Integer.parseInt(historyToken);
}
catch (Exception e)
{
}
// Do a bit of checking
if (index < 0 || index >= menu.getTabBar().getTabCount()) index = 0;
menu.selectTab(index);
}
FlowPanel page(String html)
{
// A holder panel - just a wrapper to avoid a sizing problem
FlowPanel panel = new FlowPanel();
// The main area for the page
HTML page = new HTML(html);
panel.add(page);
// Set the page size and align the text
page.setSize("400px", "400px");
page.setHorizontalAlignment(HasAlignment.ALIGN_CENTER);
// Add a border and some padding
DOM.setStyleAttribute(page.getElement(), "border", "10px solid blue");
DOM.setStyleAttribute(page.getElement(), "padding", "20px");
// Return the panel
return panel;
}
public boolean onBeforeTabSelected(SourcesTabEvents sender, int tabIndex)
{
return true;
}
public void onTabSelected(SourcesTabEvents sender, int tabIndex)
{
History.newItem("" + tabIndex);
}
}
TabPanels do not look pretty in their raw state. No colors, no lines, no spacing, no little hand over the tabs. No tabs that you can identify, just a bunch of text cramped up in the corner. I strongly recommend that you try the demo without the CSS just to appreciate the labour pains I've saved you (by nicking the CSS from KitchenSink example and tweaking it a bit).
/******************************************************
* TabPanel
******************************************************/
.gwt-TabPanel
{
}
.gwt-TabPanelBottom
{
background-color : #ffc;
border-left : 1px solid black;
border-right : 1px solid black;
height : 98%;
border-bottom : 1px solid black;
padding : 10px;
}
.gwt-TabBar
{
margin-top : 15px;
height : 100%;
}
.gwt-TabBar .gwt-TabBarFirst
{
height : 100%;
width : 25px;
padding-left : 3px;
border-bottom : 1px solid black;
}
.gwt-TabBar .gwt-TabBarRest
{
border-left : 1px solid #AAA;
border-bottom : 1px solid black;
padding-right : 3px;
}
.gwt-TabBar .gwt-TabBarItem
{
border-top : 1px solid #ccc;
border-left : 1px solid #ccc;
border-bottom : 1px solid black;
background-color : #ffa;
color : #ccc;
font-size : 70%;
font-weight : bold;
padding : 2px 8px 2px 8px;
cursor : pointer;
cursor : hand;
}
.gwt-TabBar .gwt-TabBarItem-selected
{
color : black;
background-color : #ffc;
border-top : 1px solid #aaa;
border-left : 1px solid #aaa;
border-right : 1px solid #333;
border-bottom : 0px solid black;
padding : 2px 8px 2px 8px;
cursor : default;
}
If you want to try it out for yourself, here's the download link.
RXT_History_Support-1.1.001.zip