SuperTextInput – Building a Custom Component in Flex 4
admin originally posted this on Saturnboy.
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:
- Extend – extend some default component and add some new functionality
- 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):