TerrificTabBar – Another Custom Component in Flex 4
Another day, another custom component in Flex 4. This time it’s a closeable TabBar
that I’m calling TerrificTabBar, because SuperTabBar is taken. This post is basically Part 2 of my previous post on SuperTextInput. And much like that post, TerrificTabBar follows the Enhanced Component Pattern by extending TabBar
and adding some new functionality.
Extend TabBar and TabBarButton
I just want to build your average web browser tab in Flex 4, no more, no less. Browser tabs have two cool features: each tab has a close button, and the tabs can be reordered by dragging. Actually, I just lied about the no less part, because I’m going to ignore dragging and focus my attention on the closeable tab part. Please see my previous post on drag-and-drop in Flex 4 if you are interested in the dragging part.
A TabBar
is just a bag of tabs, where each individual tab is a button, or a TabBarButton
to be more precise. To create a closeable tab, we need to add a new button that can be clicked to close the tab, duh. And this is in addition to the default behavior of clicking the tab to select it. Since we are disciples of The Flex 4 Way of building custom components, we know this means we must extend TabBarButton
and add a new Button
SkinPart
to it:
package components { ... public class TerrificTabBarButton extends ButtonBarButton { [SkinPart(required="false")] public var closeButton:Button; private var _closeable:Boolean = true; public function TerrificTabBarButton() { super(); //IMPORTANT: this enables the button's children (aka the close button) to receive mouse events this.mouseChildren = true; } [Bindable] public function get closeable():Boolean { return _closeable; } public function set closeable(val:Boolean):void { if (_closeable != val) { _closeable = val; closeButton.visible = val; labelDisplay.right = (val ? 30 : 14); } } private function closeHandler(e:MouseEvent):void { dispatchEvent(new TerrificTabBarEvent(TerrificTabBarEvent.CLOSE_TAB, itemIndex, true)); } override protected function partAdded(partName:String, instance:Object):void { super.partAdded(partName, instance); if (instance == closeButton) { closeButton.addEventListener(MouseEvent.CLICK, closeHandler); closeButton.visible = closeable; } else if (instance == labelDisplay) { labelDisplay.right = (closeable ? 30 : 14); } } override protected function partRemoved(partName:String, instance:Object):void { super.partRemoved(partName, instance); if (instance == closeButton) { closeButton.removeEventListener(MouseEvent.CLICK, closeHandler); } } } }
First, we have the closeButton
SkinPart
and it’s closeHandler()
event handler. Wiring the handler function to the button is done in the partAdded()
override, and un-wiring in the partRemoved()
override. The handler just bubbles a closeTab
custom event which will be handled by the parent TabBar
. Next, we use the closeable
property to manage the close button’s visibility. Finally, an additional button visibility check is necessary in partAdded()
to set the initial visibility.
There are two other interesting pieces of code. One, we must set mouseChildren = true
in the constructor to enable our button within a button to receive and respond to mouse events. Two, in a somewhat hackish fashion, I add or remove padding from the tab’s label when the close button is present or not.
Now that we have a nice close button on the individual tabs, we extend TabBar
to make a pretty API for managing tabs and their new functionality. Here is an abbreviated TerrificTabBar
component showing the relevant public methods:
package components { ... public class TerrificTabBar extends TabBar { public function TerrificTabBar() { super(); } public function setCloseableTab(index:int, value:Boolean):void { if (index >= 0 && index < dataGroup.numElements) { var btn:TerrificTabBarButton = dataGroup.getElementAt(index) as TerrificTabBarButton; btn.closeable = value; } } public function getCloseableTab(index:int):Boolean { if (index >= 0 && index < dataGroup.numElements) { var btn:TerrificTabBarButton = dataGroup.getElementAt(index) as TerrificTabBarButton; return btn.closeable; } return false; } private function closeHandler(e:TerrificTabBarEvent):void { closeTab(e.index, this.selectedIndex); } public function closeTab(closedTab:int, selectedTab:int):void { ... } protected override function partAdded(partName:String, instance:Object):void { super.partAdded(partName, instance); if (instance == dataGroup) { dataGroup.addEventListener(TerrificTabBarEvent.CLOSE_TAB, closeHandler); } } protected override function partRemoved(partName:String, instance:Object):void { super.partRemoved(partName, instance); if (instance == dataGroup) { dataGroup.removeEventListener(TerrificTabBarEvent.CLOSE_TAB, closeHandler); } } } }
There is a public getter and setter for toggling the close-ability of individual tabs, setCloseableTab()
and getCloseableTab()
. And also a public method for closing a tab, closeTab()
, which can be used to force close an un-closeable tab. Additionally, closeTab()
is used internally to close a tab when its close button is clicked. As is standard, the wiring and un-wiring of the closeHandler()
handler function is done in the partAdded()
and partRemoved()
overrides.
Very vanilla, but there is one interesting part worth mentioning. I made the decision to attach the listener to the dataGroup
property of TabBar
, this is the default container of individual tabs (aka TerrificTabBarButton
s). Yes, I could just as easily have attached the handler directly to TabBar in the constructor and used weak references. But after having built enough custom Flex 4 components, it feels funny to me to do any event wiring outside of partAdded()
and partRemoved()
. Thus, I made the decision to attached to dataGroup
. Without doing a bunch of testing (which I haven’t done), I can’t say if this is better or worse from a memory or performance perspective, I just like the way this code looks better.
Conclusion
I put a pretty skin on everything using some of my favorite drawing tricks, but that’s the subject of a future post. Some brief highlights include:
- I used CSS to wire my components to my skins, so I minimize the use of
skinClass
attributes for better decoupling - I used a big negative
gap
inTerrificTabBarSkin
to make the tabs overlap - I set a
maxWidth
and aminWidth
on the individual tabs inTerrificTabBarButtonSkin
, which in combination withmaxDisplayedLines="1"
on theLabel
, make the tabs truncate with...
when they get too big - The curved tab shape is drawn with a
Path
, but I wrap it with aGroup
and set theGroup
‘s scale-9 grid (scaleGridLeft
,scaleGridTop
, etc.) so it expands and contracts correctly - The close button’s x is drawn as a + around the coordinate origin, also known as (0,0), and then rotated 45 degrees
Here’s the finished product (view source enabled):
Use it and enjoy.
UPDATE: Jon Rose has written an excellent FlexMonkey Deep Dive article on how to bring a custom component under automation. He uses TerrificTabBar as an example. Thanks Jon.
Rodrigo
8.3.2010
Just a tip: Some Browsers get 2046 error, just disable RSL digest on Project Properties.
Codeflayer
8.9.2010
I am having some trouble getting this example to work. Specifically the close button is not clickable in my implementation of this. I was sure to set the
mouseChildren = true
in the constructor on the custom tab button but it is still unclickable. Any ideas on what I’m doing wrong?justin
8.9.2010
@Codeflayer: I would try:
mouseChildren = true
is working correctly, then close button will change state on hover. And we know it is getting mouse events. If not, then debug this.closeTab
event up to theTabBar
) which does the actual removing of the tab.Good luck.
Codeflayer
8.9.2010
Scratch that, I figured out what I was doing wrong. I didn’t have the custom tab bar using the custom button class, it was still using the default button class but applying the custom skin.
Dan Nelson
12.11.2010
Hope you don’t mind but I added in the drag and drop part. I based it off your TerrificTabBar component. You can find it here.
Tim
12.15.2010
I’m trying to test your TerrificTabBar but when I import the project I get an error saying that the flex builder is unable to open …\TerrificTabBar\libs What might be causing this?
Thanks,
Tim
Alexandre
12.16.2010
Hello!
I’m getting an error on this line:
labelDisplay.right = (closeable ? 30 : 14);
Error 1119: Access of possibly undefined property right through a reference with static type spark.core:IDisplayText.
If I comment that line, everything works fine, except the close button. It does nothing. Thanks!
justin
12.16.2010
@Tim & @Alexandre: The download is not a full FB4 project. If you’d like to run the download as is in FB4, you need to make a new project and unzip the download inside it. Also, I developed and tested under Flex 4.1 SDK, so I make no promises for Flex 4.5 (Hero).
Adrian
1.16.2011
Hi
First of all, thanks for this great description. It works perfect.
There is one typo I guess in
skins.TerrificCloseButtonSkin
. The defined transitions would not have any influence.But
circleFill
has a static color. The same holds forxFill
that has no alphas defined. If you want the transitions to animate, just switch the targets.Best regards, Adrian
justin
1.17.2011
@Adrian: Good catch! I totally reversed what I wanted to do. I should be animating the alpha of
circleFill
and the color ofxFill
(I have them reversed in the code).The correct transitions block in
skins.TerrificCloseButtonSkin
should be:Tim
4.22.2011
I’m doing something similar to this and it works when the custom TabBar is defined in MXML. However, if I try to create the tab bar and view stack in Actionscript (so I can specify tabs on the fly), the following line fails:
Alas,
btn
is always null. I’d be happy to share a sample based on your project if you are interested. I can’t figure it out…justin
4.22.2011
@Tim: sounds like there’s some kind of race condition getting you (aka the bar is created, but not the buttons, thus you always get null). I guess things have changed in Flex 4.5…
Mark Barton
5.16.2011
Justin thanks for the great article – I guess things changed with 4.5 – I get the same issue with the
labelDisplay.right
property (there isnt one).What would you suggest is the best way forward in passing the appropriate value to the skin. I tried binding to the
closeable
property of the component so I can perform the same logic for the right value within the actual skin but it wasn’t available – should you / can you perform this form of coupling between component and skin?Any suggestions appreciated.
Mark Barton
5.16.2011
Doh! – should have read the rest of your articles first – found the answer I needed to add
hostComponent
to the binding to get the value.Any issues using binding for this that you can see?
justin
5.16.2011
@Mark: If binding works for you, use it!
In an ideal world the component defines its skin API (aka the skinning contract). And the skin just obeys the contract to skin the component. But it doesn’t always work out like we want.
gembin
5.17.2011
very cool!
how add scrolling function like flexlib SuperTabNavigator?
justin
5.17.2011
@gembin: adding tab scrolling is on you…
The much nicer, more usable part of SuperTabNavigator is the dropdown list of all available tabs (usually pinned to the right). I’d add that first, because you get “scrolling” for free.
gembin
5.18.2011
That’s great! thanks a lot!
jadd
5.23.2011
hello, issue with the labelDisplay.right here in fb 4.5. Any solutions?
justin
5.23.2011
@jadd: To fix TerrificTabBar for Flex 4.5, just edit
TerrificTabBarButton.as
and replace:labelDisplay.right = ...
with:
That’s it. Apparently, this cast is needed in 4.5.
jadd
5.24.2011
@justin: the compiler error goes away but there is now an exception:
TypeError: Error #1009: Impossibile accedere a una proprietà o a un metodo di un riferimento oggetto null.
at components::TerrificTabBarButton/partAdded()[/Volumes/Mac OS X Data/Progetti/Web/Sviluppo/Flash/Source/Saturnboy/Source/src/components/TerrificTabBarButton.as:49]
ty.
jadd
5.24.2011
@justin: closeButton is null.
Russ
8.10.2011
I was trying to set the closeable property of the button through the data property of the button, but I can’t seem to figure out how to set the data property without accessing the button directly, and then I might as well use the set closeable function. I tried putting an object in the array
{label:"foo", data:false}
, but that didn’t work. I can’t seem to find any info on setting that data property from the dataProvider. Any thoughts?justin
8.10.2011
@Russ: you might have more luck plugging into the
dataProvider
events. For example, inTerrificTabBar
which extendsTabBar
, you could add a listener for an underlyingCollectionEvent.COLLECTION_CHANGE
. Then in the handler, you could walk the data and just callsetClosableTab()
directly.Nitin
11.22.2011
Any suggestion how to add Blinking of tab on some event within the Tab’s child say a datagrid updated with new values?
justin
11.23.2011
@nitin: add a
setBlinkable(tabIdx:int)
method to the TabBar, then use events to wired everything up.