Blocking TabBar Click/Change Events

12.15.2008

One scenario that often presents itself when developing a Flex application is blocking users from navigating away from the current view.  You might want to do this if the user’s form data is incomplete or if data has changed and you want to ensure they want to discard or save their changes.

When the TabBar comes into play, blocking tab changes isn’t always straightforward.  The TabBar doesn’t dispatch any type of TabChanging event that can be canceled.  Nothing else is really offered in the way of blocking the action either.  To make matters worse, a click on a tab in the tabbar actually dispatches two different click events, one of which is a “simulated click trigger event.”

My purpose isn’t to go into the shortcomings of the TabBar component or its inner workings.  Instead, I’ll offer the BlockableTabBar for your use:

package com.mediarain.controls
{
	import flash.events.MouseEvent;
 
	import mx.core.mx_internal;
 
	import mx.controls.Button;
	import mx.controls.TabBar;
 
	use namespace mx_internal;
 
	/**
	 * Extends the functionality of TabBar by exposing a <code>condition</code> function property
	 * that will be evaluated before the tab changes.  The behavior will only be allowed if the
	 * condition is met.
	 * 
	 * @author Aaron Hardy - http://aaronhardy.com
	 */
	public class BlockableTabBar extends TabBar
	{
		/**
		 * The condition that allows/disallows the click event to proceed.  This must be a 
		 * function that returns a Boolean value.
		 */
		public var condition:Function;
 
		/**
		 * @private
		 * A flag indicating whether the tabbar's simulated click has been blocked due to the
		 * blocking condition failing.  This is used later when the real click comes through
		 * so it can also be blocked without evaluating the condition again.  This not only
		 * cuts down processing but the developer can show an alert within the condition
		 * without it being displayed multiple times.
		 */
		private var simulatedClickBlocked:Boolean = false;
 
		/**
		 * Adds a click handler to intercept click events.
		 */
		public function BlockableTabBar()
		{
			super();
			addEventListener(MouseEvent.CLICK, clickInterceptor, true);
		}
 
		/**
		 * @private
		 * Intercepts click events on the tab bar and evaluates whether the condition has
		 * been met to allow the tabs to be changed.  If the condition is not met, the event
		 * is ignored.  The simulatedClickTriggerEvent handling and the need for the two
		 * conditions is due to the functinality in the underlying tabbar architecture. 
		 */
		protected function clickInterceptor(event:MouseEvent):void
		{
			if (simulatedClickTriggerEvent && event.target is Button && 
	    			condition != null && !condition())
	    		{
	    			event.stopImmediatePropagation();
		    		event.preventDefault();
	    			simulatedClickBlocked = true;
			} 
			else if (!simulatedClickTriggerEvent && simulatedClickBlocked)
	    		{
	    			event.stopImmediatePropagation();
	    			event.preventDefault();
	    			simulatedClickBlocked = false;
	    		}
		}
	}
}

The idea here is that any mouse click on a tab is captured on the “capture” event phase.  In other words, we’re intercepting the mouse click before the TabBar can get to it and act on it.  In order for the event to continue on its way, we will execute the “condition” property, which is a function that returns a boolean value.  This property is the one you as a developer will set and should be a reference to a function that contains the logic evaluating whether the user should be allowed to move away from the current tab.  If the function returns a value of true, it will let the event pass on and the the user will be taken to the clicked tab as usual.  If the function returns a value of false, it will prevent the event from continuing on and will prevent the user from leaving the current tab.

If you’re dealing with a TabNavigator class instead of the TabBar class, my comrade Nate has a post that could help in that respect. See his post: Preventing the TabNavigator from changing tabs when one is clicked.

Hope that helps!  Let me know if you have bugs, suggestions, or questions.

Tags: , , , , ,


Comments

12.15.2008 / Preventing the TabNavigator from changing tabs when one is clicked. « Nate’s Code Vault said:

[...] for a good solution to prevent a TabBar from changing instead of the TabNavigator, check out Aaron Hardy’s blog. He used it in a project we did together and it worked [...]

03.26.2009 / Ravi said:

the link for the tabnavigator thingy is broken…it’s saying that the authors have deleted the blog. Can you please post any updated link or a solution for the tabnavigator??
I really need a solution for preventing the default tab change behaviour of tabnavigator…i tried the stopImmediatepropogation, preventDefault…but they’re of no use..

ravi.

03.26.2009 / Ravi said:

Never mind. I found the link from your blogroll…thanks :) :
http://natescodevault.com/?p=43

03.26.2009 / Aaron Hardy said:

@Ravi

Thanks for the notice. I’ve updated the link. Glad you could find it on your own though! Good luck!

05.30.2009 / Preventing the TabNavigator from changing tabs when one is clicked. | Nate's Code Vault said:

[...] for a good solution to prevent a TabBar from changing instead of the TabNavigator, check out Aaron Hardy’s blog. He used it in a project we did together and it worked [...]

12.10.2009 / Jon said:

Thanks Aaron,

Nice, clean implementation.

02.04.2010 / Craig said:

Utilizing your idea above, I was able to send the prevented event out of the listener as a parameter of the condition function. My idea was to re-dispatch the event after certain other things were done in other functions. I added a boolean in your class to simply toggle an Allow/Check behavior, which is working fine.

My issue is that when I am in a different function and I dispatch the event (seems to be identical to the one initially blocked) via one of the following

dispatchEvent(nameOfEvent);
or
nameOfBlockedTabBarObjectInstance.dispatchEvent(nameOfEvent);

The event does not seem to trigger the normal behavior in the tabBar, or at least no longer is seen by the event listener in your class.

This may be out of scope, but could you give any insight, it would mean a lot.

02.04.2010 / Aaron Hardy said:

@Craig,

I think what you’re saying is that your conditional function does stuff asynchronously, that is, the processing it’s doing cannot return an immediate true or false. This is a common scenario if you want to, say, ask the user whether they want to discard changes that have been made to a form on the current view. In this case you would return false immediately from your conditional function and store the location (child or index) they were attempting to go to (how to get that info is another discussion). If the user responds that they want to continue with the navigation, you just set selectedIndex or selectedChild to whatever they were attempting to go to in the first place. If they choose to not continue with the navigation, you don’t do anything. If you take this approach, you shouldn’t have to do any re-dispatching. Does that help?

02.05.2010 / Craig said:

Thanks for the reply so quickly, this will be a big help.

You’re suggestion was the second idea I had for the solution in fact. My issue there is that I can’t think of the correct commands to pull out the original target index from just the event object. I recently saw someone show the code on thier site, but can’t pull it up again.

Do you think you could post the best way to pull the index of the original target of the event we were capturing?

02.05.2010 / Craig said:

I think I found it, I can share it here and save you the time.

var selectedIndex:uint= TabBar(event.target.parent).getChildIndex(event.target as DisplayObject);

you use that on the event you captured and saved from within your class’ listener. So in the case of your class, you should use your class name instead of the standard class name, like so.

var selectedIndex:uint= BlockableTabBar(testMouseEvent.target.parent).getChildIndex(testMouseEvent.target as DisplayObject);

Then you can take it as a selected index and manually force the tabbar to change any time you want.

One last question though. You might know it off the top of your head, but how could I best adapt your class to the LinkBar component? An example might help too.

02.05.2010 / Aaron Hardy said:

@Craig

Your method of finding the destination index works. You can simplify it a bit: var selectedIndex:uint= TabBar(event.currentTarget).getChildIndex(event.target as DisplayObject);

Also, I tried redispatching the event like you were attempting to do before. It works fine but you need to dispatch the event off the object that dispatched it in the first place rather than the tabbar itself. Just do this: myEvent.target.dispatchEvent(myEvent);

As far as the LinkBar goes, it’s essentially the same concept but should actually be easier because I don’t think it has any “simulated click trigger” event to deal with. I’d start by overriding LinkBar’s clickHandler() function. Then I’d only call super.clickHandler() if the conditional function returned true, that is, it’s okay to proceed with the navigation.

Hope that helps.

04.14.2010 / Doug said:

Hello, Aaron

In Flex 3, my mouseOvers & mouseOuts on my LinkBar will not operate until the LinkButtons on the LinkBar have been clicked. Is there a way to get my Overs and Outs to operate after my page loads? My LinkBar is using a dataProvider to display the LinkButtons. Thank You.

04.14.2010 / Aaron Hardy said:

@Doug
I just did a simple test and the mouseover and mouseout events seem to be working fine even before clicking a link on a linkbar.

04.15.2010 / Doug said:

Hey, Aaron I was, too, brief with my description of my problem; however, another Flex expert has already solved my rollOver problem. Thank you so much. Do you know anything about loading RSS feed containing images and text from a remote Website to a Flex 3 application, and the content update everyday?

04.15.2010 / Aaron Hardy said:

I know something, but your best bet is to post your question to FlexCoders: http://tech.groups.yahoo.com/group/flexcoders/

04.15.2010 / Doug said:

Thank you very much, Aaron.

08.11.2010 / Kevin said:

I am trying to use BlockableTabBar and I am having trouble with “condition.” In my file where I am implementing the control i set condition=”tabChange()” where tabChange returns boolean. When I run my code I get TypeError: Error #1034: Type Coercion failed: cannot convert false to Function.

I have BlockableTabBar as a separate AS class. Am I supposed to implement my logic in the class? I am very new to Flex development and on my current project I am handling mostly the .NET middle tier.

Thanks,
Kevin

08.11.2010 / Aaron Hardy said:

Kevin, try replacing this:

condition=”tabChange()”

with this:

condition=”{tabChange}”

As you have it, it will be executing the function tabChange() and sending the return value (in your case, false) into the condition property. What you want is the actual function reference to be set into the condition property so it can be executed at a later time. Let me know how that works out for you.

In response to your other question, you could implement your condition logic inside the same BlockableTabBar class, but I wouldn’t recommend it. Having the condition logic outside lets you keep the BlockableTabBar class generic and usable in many different contexts.

08.11.2010 / Kevin said:

All this time…. I actually thought I had tried it without the parentheses but apparently I did not. That did the trick. condition={tabChange}

Your explanation was spot on. It was calling the function and getting the result.

Thanks!


Leave a Comment

Your email address is required but will not be published.




Comment