Building a Tab control using Activator – Part III
In the first part in this series we built a very simple tab widget which met the initial requirements, and then was extended with a number of options to increase re-usability in the second part. Amongst the options were a number for events, which I will discuss in this third part.
In the options we have specified a number of events. Most of these events are fired by the activator widget and require no interference from us (other than to listen to the changeOn event). However, there are two events that the tabs widget will fire: changing, which is cancellable; and changed.
Prototype provides a custom event model which works just like real browser events. Elements have a fire method which is used to fire these custom events. Custom event names must be made up of two parts separated by a colon. Generally the first part is a noun describing the element that fired the event and the second part a verb describing what is happening. So the tabs widget will fire “tab:changing” when a tab has been clicked and “tab:changed” after the change has happened. By listening for these events, your code can react to changes in the state of the tabs widget.
Custom events can send additional information to event handlers in the memo of the event. For example with the tabs widget we would want to send which tab is being changed from and which tab to. It is also useful, when possible, to pass the original DOM event that triggered the custom event, because DOM events contain additional properties such as ctrlKey and altKey which your event handlers might want access to. Finally, it is often convenient to pass along the widget itself.
Now the activator widget already has observe, stopObserving and fire methods which work on the container element when a container is specified and the document otherwise. Our tabs widget always specifies a container, so there’s no need to reinvent the wheel, we’ll just steal these methods directly from the activator:
initialize: function(element, options) {
//snip
this.observe = this.activator.observe.bind(this.activator);
this.stopObserving = this.activator.stopObserving.bind(this.activator);
this.fire = this.activator.fire.bind(this.activator);
this.observe(this.activator.options.events[this.options.changeOn], this.show.bind(this));
//snip
},
As you can see, this also means that the this.activator.observe has become just this.observe.
In the show method we want to fire the two events. The changing event should be cancellable, meaning that an event handler can cancel the event and prevent the change of the tabs. Prototype extends event objects with a stop method. On DOM events this calls the built-in stopPropogation and preventDefault methods. As a side effect it also sets a stopped property on the event object which can then be tested for
The show method ends up looking like this:
show: function(e) {
var tab;
if(Object.isNumber(e) || Object.isString(e)) {
tab = this.activator.getElement(e);
} else {
tab = this.activator.isElement(e.memo.element);
}
if(!tab) return;
var memo = { from: this.selected, to: tab, tabs: this, event: e.memo ? e.memo.event : null };
var ev = this.fire(this.options.events.changing, memo);
if(ev.stopped) return;
if(this.selected) {
this.activator.setSelected(this.selected, false);
$$(this.options.linkMethod(this.selected.down(this.options.linkSelector))).invoke("hide");
}
this.selected = tab;
this.activator.setSelected(this.selected, true);
$$(this.options.linkMethod(this.selected.down(this.options.linkSelector))).invoke("show");
this.fire(this.options.events.changed, memo);
},
After getting the tab that should be shown, the method sets up the event memo which includes the tab being changed from, changed to, the tabs widget itself and the DOM event that caused the change if it exists (if it doesn’t exist that means the change is being caused programmatically by a call to show with an index or id). It then fires the changing event and checks to see if any event handler has stopped the event. Next the actual change is performed and finally the changed event is fired.
Why would you want to cancel a tab changing event? Well, in an Ajax application you might have a form on a particular tab that needs to be completed before another tab can be accessed. Or you might just want to confirm some changes before changing tabs. The changed event can be used to load data dynamically via Ajax or JSON. Or you might use it to change some other aspects of the page like headings or help text. But here is an example of using it to animate the tabs changing using the effects from Script.aculo.us.
var subnav = new Widget.Tabs("subnavigation", {
tabSelector: "span",
linkSelector: "a",
linkMethod: Widget.Tabs.classLinks
});
subnav.observe(subnav.options.events.changing, function(e) {
e.stop();
e.memo.tabs.selected = e.memo.to;
new Effect.SlideUp(e.memo.tabs.panels(e.memo.from)[0], {
duration: 0.2,
queue: {
scope: "subnav-tabs",
position: "end"
},
afterFinish: function() {
e.memo.to.setSelected();
}
});
new Effect.SlideDown(e.memo.tabs.panels(e.memo.to)[0], {
duration: 0.3,
delay: 0.2,
queue: {
scope: "subnav-tabs",
position: "end"
}
});
});
The first statement creates the tabs control with a few options as we have seen a couple of times now. The second part is where it gets interesting. Here we are observing the changing event. In the observer function we stop the event which means the tabs control doesn’t proceed further in changing the tabs. Since we are going to change the tab anyway, we set the selected tab in the control to the new tab anyway. This prevents the control from getting confused about which is the selected tab. We then set up a pair or animations to hide the current tab and then show the selected on. Finally, after the first animation is finished, we change the selected tab button.
And what does all this look like? I have a demo page here.
Finally, from a functionality point of view, we need a destructor. Destructor’s are often left out of JavaScript widgets based on the idea that the widgets will be destroyed when the page unloads. With large Ajax applications however, you may be creating and destroying loads of widgets without ever leaving the page. Destroying a widget should force it to clean up after itself, remove event listeners and restore (if possible) the DOM to it’s original state.
In this case the activator widget already has a destroy method that cleans up its event handlers, so the only thing left for the tabs control to do is to show all the hidden panels. In the end the destroy method looks like this:
destroy: function() {
this.activator.getElements("*").each(function(e) {
this.panels(e).invoke("show");
}, this);
this.activator.destroy();
}
which simply shows all the panels and then destroys the activator.
And there we have it: a complete widget. But even now there’s more to do if you want to release it into the wild. We must add documentation, a page of demos, package the whole thing in a nice, easy to use zip file, and maybe submit it to a few sites. I will deal with all that in the fourth and final installment in this series.

There is no great genius without some touch of madness.


