Saturnboy
 7.22

SuperTextInput – Building a Custom Component in Flex 4

, ,

I’ve been building a lot of Flex 4 custom components lately, including a sliding drawer, a multiple content area container, and now SuperTextInput. Nor will this be that last, because I think I have a few more in me (update: see TerrificTabBar). I thought it would be useful to spend some time in the details, explaining The Flex 4 Way and how I try to walk the path.

SuperTextInput is a prompting, clearable TextInput extension in Flex 4. It’s just an enhanced version of the default TextInput control, and as such, it follows a fairly standard pattern of custom component creation.

Enhanced Component Pattern

It’s almost too stupid to call this a pattern, but it’s so common in custom component creation that I’ll run with it. Also, I’ve found it to be worthwhile to distinguish between adding new functionality to a component already present in the framework (aka an enhanced component) versus creating a truly custom component.

The enhanced component pattern is just two simple steps:

  1. Extend – extend some default component and add some new functionality
  2. Skin – make it look good

In my version of reality, these steps carry equal weight, because almost all worthwhile functionality in Flex touches the UI in some fashion, so the design and UX (the look-and-feel, it’s usability, the integration into the rest of the app, etc.) are critical. Don’t forget or skimp on step #2 because it’s all the client, team, customer ever sees.

A Prompting TextInput

Since SuperTextInput has two new pieces of functionality (the prompt and the clear button), I’ll split them apart, and consider each part separately. First, the prompt is merely the text you see when the TextInput is empty. It often becomes a space saving label, because it can be used to tell the user what goes into the TextInput without costing the UI any screen real estate.

Thinking more about the prompt, we want the prompt text to be visible initially, but it should disappear when the user clicks (or tabs) to the control, and only returns when the control loses focus and is still empty. So this tells us that we need to communicate both the prompt text and it’s visibility to our skin. The prompt text can just be a simple Label SkinPart, but it’s visibility is complicated enough that it makes sense to add a new prompting SkinState.

Here’s a functioning PromptingTextInput custom component (which is simply the prompting code lifted from SuperTextInput.as):

package components {
    import flash.events.FocusEvent;
    import mx.events.FlexEvent;
    import spark.components.Label;
    import spark.components.TextInput;
    import spark.events.TextOperationEvent;
 
    [SkinState("prompting")]
    public class PromptingTextInput extends TextInput {
 
        [SkinPart(required="false")]
        public var promptDisplay:Label;
 
        private var _prompt:String = '';
        private var _focused:Boolean = false;
 
        public function PromptingTextInput() {
            super();
 
            //watch for programmatic changes to text property
            this.addEventListener(FlexEvent.VALUE_COMMIT, textChangedHandler, false, 0, true);
 
            //watch for user changes (aka typing) to text property
            this.addEventListener(TextOperationEvent.CHANGE, textChangedHandler, false, 0, true);
        }
 
        [Bindable]
        public function get prompt():String {
            return _prompt;
        }
        public function set prompt(value:String):void {
            if (_prompt != value) {
                _prompt = value;
                if (promptDisplay != null) {
                    promptDisplay.text = value;
                }
            }
        }
 
        private function textChangedHandler(e:Event):void {
            invalidateSkinState();
        }
 
        override protected function focusInHandler(event:FocusEvent):void {
            super.focusInHandler(event);
            _focused = true;
            invalidateSkinState();
        }
        override protected function focusOutHandler(event:FocusEvent):void {
            super.focusOutHandler(event);
            _focused = false;
            invalidateSkinState();
        }
 
        override protected function partAdded(partName:String, instance:Object):void {
            super.partAdded(partName, instance);
 
            if (instance == promptDisplay) {
                promptDisplay.text = prompt;
            }
        }
 
        override protected function getCurrentSkinState():String {
            if (prompt.length > 0 && text.length == 0 && !_focused) {
                return 'prompting';
            }
            return super.getCurrentSkinState();
        }
    }
}

In addition to the promptDisplay SkinPart and the new prompting SkinState, there is a lot of other stuff going on in the above code. First, as is typical with data-driven SkinParts, we back the promptDisplay with a good old prompt property. The net is the fairly common pattern of: check if the SkinPart is not null, then do something to it. So in the prompt setter, we assign the incoming value to the private _prompt variable, then check if promptDisplay is available and if yes, set it’s text property. The setter does the job of updating the prompt, but only once everything is happily running. In order to get the data to the skin initially, we must use the partAdded() override to pass the local prompt to the promptDisplay‘s text property. And that’s it for the prompt text.

The prompt visibility part requires lots of event watching, and also SkinState stuff because we made the choice to push visibility via the prompting SkinState. First, we wire up both the programmatic text change events and the user text change events to a handler, textChangedHandler(), that does nothing more than invalidate the state. TextInput change events are a little wacky, but the code works fine. Next, instead of wiring the focus events to another handler (as seen in this prompting TextInput component by Andy McIntosh), we simply override the protected handlers in the parent and add our focus-tracking logic directly. Finally, we override getCurrentSkinState() to do the work of figuring out whether or not the prompt should be displayed.

A skin for PromptingTextInput is now trivial because our component does the work of pushing the important information to the skin. If we ignore all the pretty stuff, the skin is very simple:

<?xml version="1.0" encoding="utf-8"?>
<s:SparkSkin ...>
    ...
    <s:states>
        <s:State name="normal"/>
        <s:State name="prompting"/>
        <s:State name="disabled"/>
    </s:states>
 
    <s:RichEditableText id="textDisplay" ... />
    <s:Label id="promptDisplay" includeIn="prompting" ... />
</s:SparkSkin>

We add the prompting State to the list of states and also add the promptDisplay Label component. By using the standard inline state syntax, includeIn="prompting" our Label is shown only in the prompting state.

A Clearable TextInput

The second piece of SuperTextInput functionality is the clear button. The clear button appears when the TextInput has a value, and when clicked, it clears that value (which re-displays the prompt). Again, there are two pieces of information the need to be communicated to the skin to create the clear button functionality: the button itself and it’s visibility. In this case, since the visibility is so simple (on if TextInput has a value, otherwise off), we’ll just punt and manage it directly in the component. Therefore, the only a Button SkinPart for the clear button will be pushed to the skin.

Here’s a functioning ClearableTextInput custom component (which is simply the clear button code lifted from SuperTextInput.as):

package components {
    import flash.events.Event;
    import flash.events.MouseEvent;
    import mx.events.FlexEvent;
    import spark.components.Button;
    import spark.components.TextInput;
    import spark.events.TextOperationEvent;
 
    public class ClearableTextInput extends TextInput {
 
        [SkinPart(required="false")]
        public var clearButton:Button;
 
        public function ClearableTextInput() {
            super();
 
            //watch for programmatic changes to text property
            this.addEventListener(FlexEvent.VALUE_COMMIT, textChangedHandler, false, 0, true);
 
            //watch for user changes (aka typing) to text property
            this.addEventListener(TextOperationEvent.CHANGE, textChangedHandler, false, 0, true);
        }
 
        private function textChangedHandler(e:Event):void {
            if (clearButton) {
                clearButton.visible = (text.length > 0);
            }
        }
 
        private function clearClick(e:MouseEvent):void {
            text = '';
        }
 
        override protected function partAdded(partName:String, instance:Object):void {
            super.partAdded(partName, instance);
 
            if (instance == clearButton) {
                clearButton.addEventListener(MouseEvent.CLICK, clearClick);
                clearButton.visible = (text != null && text.length > 0);
            }
        }
 
        override protected function partRemoved(partName:String, instance:Object):void {
            super.partRemoved(partName, instance);
 
            if (instance == clearButton) {
                clearButton.removeEventListener(MouseEvent.CLICK, clearClick);
            }
        }
    }
}

After the PromptingTextInput, the ClearableTextInput is a little more straightforward. First, we have the clearButton SkinPart and it’s clearClick() event handler. Wiring the handler function to the button is done in the partAdded() override, and un-wiring in the partRemoved() override. Next, button visibility is managed by watching for both programmatic text change events and user text change events. The handler, textChangedHandler(), sets the button as visible when the control has text in it.

As I mentioned above, I decided against pushing the clearButton‘s visibility down to the skin via a SkinState, and instead chose to manage it inside the component by setting clearButton.visible directly. I tend to favor the SkinState method when more than one thing needs to change in the skin or if I need advanced visuals (like transitions). If I need to do just one thing and I don’t care about visuals, I’ll do it inside the component. The two examples here aren’t the best to illustrate the two options, but that’s my general thought process when building a custom component and custom skin.

A skin for ClearingTextInput is super trivial. Again, ignoring all the pretty stuff, the skin is:

<?xml version="1.0" encoding="utf-8"?>
<s:SparkSkin ...>
    ...
    <s:states>
        <s:State name="normal"/>
        <s:State name="disabled"/>
    </s:states>
 
    <s:RichEditableText id="textDisplay" ... />
    <s:Button id="clearButton" ... />
</s:SparkSkin>

Just add the clearButton Button and position it.

Fusion, Glorious Fusion

The fusion process of creating SuperTextInput from PromptingTextInput and ClearableTextInput is nothing more than copy and paste. SuperTextInput has lots of uses, but my favorite is to use it to capture text input to filter a list. It also works great as a search box, or in any smart form UI. Enjoy.

Here’s the finished product showing all three custom components skinned and ready for action (view source enabled):

Flash is required. Get it here!
Files

Comments

Aseem Behl

7.24.2010

1

This is a excellent tutorial. Thanks!

Erik

8.1.2010

2

When I try to use your component in my application — the whole screen goes white. Even the pre-loader disappears. Seems to work fine when I compile the sample app though. Any idea where the bug may be?

Erik

8.1.2010

3

Spoke to soon. I didn’t see this warning:

CSS type selectors are not supported in components: ‘com.saturnboy.SuperTextInput.components.SuperTextInput’

I just moved it out of the Style sheet and used the skinClass instead.

If there is a better way to do this I’d love to hear about it…

8.3.2010

4

@Erik: All of my example code is typically packaged one level deep (aka I don’t prefix everything with com.saturnboy). So, components are in components, skins are in skins, etc.

So if you re-package the SuperTextInput component you need to change the package name in four places:

  1. In the component itself: the package directive.
  2. In the skin: the HostComponent, and optionally in the skin’s code if there are any skinClass attributes.
  3. In your code: the xmlns:components namespace
  4. In the CSS: the @namespace components namespace

For your error, you must have missed one of the above. Good luck.

8.4.2010

5

Great component!

I am developing a component similar to other features and possibilities. If you want to contribute in some way, or just give such a component, take a look here: http://github.com/brust/SearchInput

brianj774

11.29.2010

6

@Erik & @Justin…

I am having this same issue, but on following the suggestions made by Justin, and still having no luck at all, I did some digging, and found some helpful info from polygeek.

He says, “That is a Type selector and so will work but only in the Application root. If you try that from inside another component then you’ll see a warning: CSS type selectors are not supported in components: ‘whateverComponentYouAreIn’. You will have to take the above CSS and place it in <s:Application> or in a CSS file that is declared in the <s:Application>.”

11.29.2010

7

@brianj774: Yes. Sorry I wasn’t clear. You must wire skins to components in your application’s CSS. As a component author, I can’t do this wiring for you.

higgins

12.27.2010

8

Can you explain why the Clear Button is always on the right? I’m trying to get it on the left of the field, is this a SkinPart issue or is that position set somewhere and I’m not seeing it?

Thanks.

12.27.2010

9

@higgins: All positioning is done in the skin itself (as it should be). You should just create a “ClearRightSkin” and swap the left/right attributes of the text field and the button.

4.1.2011

10

excellent tutorial and code samples – thank you very much.

Just a little information for the ones using the input in conjunction with a filterFunction (e.g. ArrayCollection). In the clearClick() function you need to dispatch a TextOperationEvent.CHANGE event manually. (Maybe already clear for everyone)

Erik

9.9.2011

11

Seems to have broken with Flex 4.5.1. It throws a bunch of compile errors like:

Description Resource Path Location Type
1024: Overriding a function that is not marked for override. PromptingTextInput.as /Flex/src/com/saturnboy/SuperTextInput/components line 30 Flex Problem

davi

11.22.2011

12

@Rico, yep, to have this component working with the filterFunction, you must dispatch the TextOperationEvent.CHANGE event!

private function clearClick(e:MouseEvent):void {
    text = "";
    dispatchEvent(new TextOperationEvent(TextOperationEvent.CHANGE));
}

Jimmy Johns

1.10.2012

13

I upgraded to Flash Builder 4.6 and now get the following errors:

“Overriding a function that is not marked for override”

That error is at Line 1 which is:

package dashboard.components {

and line 35, which is:

public function get prompt():String {

Then, I get another error:

“A conflict exists with inherited definition spark.components.supportClasses:SkinnableTextBase.promptDisplay in namespace public.”

This is at line 16, which is:

public var promptDisplay:Label;

davi’s solution above was unsuccessful in fixing this problem. Please help!

soham

2.17.2012

14

the component was awesome, but same error here on flex 4.6… how to resolve this? thanks

© 2014 saturnboy.com