Saturnboy
 8.2

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 TerrificTabBarButtons). 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 in TerrificTabBarSkin to make the tabs overlap
  • I set a maxWidth and a minWidth on the individual tabs in TerrificTabBarButtonSkin, which in combination with maxDisplayedLines="1" on the Label, make the tabs truncate with ... when they get too big
  • The curved tab shape is drawn with a Path, but I wrap it with a Group and set the Group‘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):

Flash is required. Get it here!

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.

Files

Comments

8.3.2010

1

Just a tip: Some Browsers get 2046 error, just disable RSL digest on Project Properties.

Codeflayer

8.9.2010

2

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?

8.9.2010

3

@Codeflayer: I would try:

  1. If 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.
  2. If hover is working, but click is not, then the prime suspect is the click handler. Debug the wiring of the close button’s click handler.
  3. If the close button’s click handler is working, the next suspect is the outer close tab handler (remember the close button throws a closeTab event up to the TabBar) which does the actual removing of the tab.

Good luck.

Codeflayer

8.9.2010

4

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.

12.11.2010

5

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

6

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

7

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!

12.16.2010

8

@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

9

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 for xFill that has no alphas defined. If you want the transitions to animate, just switch the targets.

Best regards, Adrian

1.17.2011

10

@Adrian: Good catch! I totally reversed what I wanted to do. I should be animating the alpha of circleFill and the color of xFill (I have them reversed in the code).

The correct transitions block in skins.TerrificCloseButtonSkin should be:

<s:transitions>
    <s:Transition fromState="up" toState="over">
        <s:Parallel duration="250">
            <s:AnimateColor target="{xFill}" />
            <s:Animate target="{circleFill}">
                <s:SimpleMotionPath property="alpha" />
            </s:Animate>
        </s:Parallel>
    </s:Transition>
    <s:Transition fromState="over" toState="up">
        <s:Parallel duration="250">
            <s:AnimateColor target="{xFill}" />
            <s:Animate target="{circleFill}">
                <s:SimpleMotionPath property="alpha" />
            </s:Animate>
        </s:Parallel>
    </s:Transition>
</s:transitions>

Tim

4.22.2011

11

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:

var btn:TerrificTabBarButton = dataGroup.getElementAt(index) as TerrificTabBarButton;

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…

4.22.2011

12

@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…

5.16.2011

13

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.

5.16.2011

14

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?

5.16.2011

15

@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

16

very cool!
how add scrolling function like flexlib SuperTabNavigator?

5.17.2011

17

@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

18

That’s great! thanks a lot!

jadd

5.23.2011

19

hello, issue with the labelDisplay.right here in fb 4.5. Any solutions?

5.23.2011

20

@jadd: To fix TerrificTabBar for Flex 4.5, just edit TerrificTabBarButton.as and replace:

labelDisplay.right = ...

with:

(labelDisplay as Label).right = ...

That’s it. Apparently, this cast is needed in 4.5.

jadd

5.24.2011

21

@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

22

@justin: closeButton is null.

Russ

8.10.2011

23

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?

8.10.2011

24

@Russ: you might have more luck plugging into the dataProvider events. For example, in TerrificTabBar which extends TabBar, you could add a listener for an underlying CollectionEvent.COLLECTION_CHANGE. Then in the handler, you could walk the data and just call setClosableTab() directly.

Nitin

11.22.2011

25

Any suggestion how to add Blinking of tab on some event within the Tab’s child say a datagrid updated with new values?

11.23.2011

26

@nitin: add a setBlinkable(tabIdx:int) method to the TabBar, then use events to wired everything up.

© 2014 saturnboy.com