Example: Creating a skinnable Spark component

Composite components are components that contain multiple subcomponents. They might be graphical assets or a combination of graphical assets and component classes. For example, you can create a component that includes as subcomponents a button, rich text field, and a border graphic. Or, you can create a component that includes a text field and a validator.

When you create composite components, you define the subcomponents inside the component’s skin class. You must plan the layout of the subcomponents that you are including, and set properties such as default values in the skin class.

Properties of the individual subcomponents are not directly accessible in MXML. For example, if you create a component that extends the SkinnableComponent class and uses a Button and a RichEditableText component as subcomponents, you cannot set the Button control’s label property in MXML.

Instead, you can define a myLabel property on the custom component that is exposed in MXML. When the user sets the myLabel property, your custom component can set that property on the Button.

Creating the ModalText component

This example component, called ModalText and defined in the file ModalText.as, combines a Button control and a RichEditableText component. You use the Button control to enable or disable text input in the RichEditableText component.

This control has the following attributes:

  • By default, you cannot edit the RichEditableText component.

  • Click the Button control to toggle editing of the RichEditableText component.

  • Use the ModalText.text property to programmatically write content to the RichEditableText component.

  • Editing the text in the RichEditableText component dispatches the change event.

  • Use the text property as the source for a data binding expression.

The following is an example MXML file that uses the ModalText control:
<?xml version="1.0" encoding="utf-8"?>
<!-- asAdvancedSpark/SparkMainModalText.mxml -->
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:MyComp="myComponents.*">
    
    <MyComp:ModalText/>
 
</s:Application>

The executing SWF file for the previous example is shown below:

Component users cannot directly access the properties of the Button and RichEditableText subcomponents. However, they can use descendant selectors to set the styles on the subcomponents, as the following example shows:
<?xml version="1.0" encoding="utf-8"?>
<!-- asAdvancedSpark/SparkMainModalTextStyled.mxml -->
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:MyComp="myComponents.*">
    
    <fx:Style>
        @namespace MyComp "myComponents.*";
        @namespace s "library://ns.adobe.com/flex/spark";

        MyComp|ModalText s|Button {
            chromeColor: #663366;
            color: #9999CC;
        }
    </fx:Style>
    
    <MyComp:ModalText/>
 
</s:Application>

The executing SWF file for the previous example is shown below:

In this example, you use descendent selectors to set the set the chromeColor and color styles of the Button subcomponent. For more information, see Using Cascading Style Sheets.

Defining event listeners for composite components

Subcomponents can dispatch events. The main component can either handle the events internally, or propagate the event so that a component user can handle them.

Custom components implement the partAdded() method to create children of the component, as the following example shows:

override protected function partAdded(partName:String, instance:Object):void { 
    super.partAdded(partName, instance); 
    if (instance == textInput) { 
        textInput.editable = false; 
        textInput.text= _text; 
        textInput.addEventListener("change", textInput_changeHandler); 
    } 
    if (instance == modeButton) { 
        modeButton.label = "Toggle Editing Mode"; 
        modeButton.addEventListener("click", modeButton_changeHandler); 
    } 
}

The partAdded() method contains a call to the addEventListener() method to register an event listener for the change event generated by the RichEditableText subcomponent, and for the click event for the Button subcomponent. These event listeners are defined within the ModalText class, as the following example shows:

// Handle events for a change to RichEditableText.text property. 
private function textInput_changeHandler(eventObj:Event):void { 
        dispatchEvent(new Event("change")); 
} 
 
// Handle the click event for the Button subcomponent. 
private function modeButton_changeHandler(eventObj:Event):void { 
        text_mc.editable = !text_mc.editable; 
}

You can handle an event dispatched by a child of a composite component in the component. In this example, the event listener for the Button subcomponent’s click event is defined in the class definition to toggle the editable property of the RichEditableText subcomponent.

However, if a child component dispatches an event, and you want that opportunity to handle the event outside of the component, you must add logic to your custom component to propagate the event. Notice that the event listener for the change event for the RichEditableText subcomponent propagates the event. This lets you handle the event in your application, as the following example shows:

<?xml version="1.0"?> 
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:MyComp="myComponents.*"> 
 
    <fx:Script> 
        <![CDATA[ 
 
            import flash.events.Event; 
 
            function handleText(eventObj:Event) 
            { 
                ... 
            } 
        ]]> 
    </fx:Script> 
 
    <MyComp:ModalText change="handleText(event);"/> 
</s:Application>

Creating the skin class for the ModalText component

The ModalText component defines a Button subcomponent and a Rich TextArea subcomponent. Each of these subcomponents is defined in the ModalText.as file as a required skin part.

A component uses CSS styles to specify the skin class that implements the skin parts. The ModalText component uses the setStyle() method to set the style. However, calling the setStyle() method in the component definition does not make it easy to reskin the component. You can also use an external style sheet or other mechanism to set the style that defines the skin class.

The skin class performs the following:
  • Uses the [HostComponent] metadata tag to specify ModalText as the host component of the skin.

  • Defines a single view state named normal.

  • Uses the Rect class to draw a border around the RichTextArea subcomponent.

  • Uses a Scroller component to add scroll bars to the RichTextArea subcomponent.

The skin class is defined in MXML, as the following example shows:

<?xml version="1.0" encoding="utf-8"?>
<!-- asAdvancedSpark\myComponents\ModalTextSkin.mxml -->
<s:SparkSkin xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    minWidth="100" minHeight="25">

    <!-- Define ModalText as the host component of the skin. --> 
    <fx:Metadata>
        <![CDATA[ 
            [HostComponent("myComponents.ModalText")]
        ]]>
    </fx:Metadata> 

    <!-- Define the normal view state. --> 
    <s:states>
        <s:State name="normal"/>
    </s:states>

    <!-- Define the border around the RichEditableText subcomponent. --> 
    <s:Rect x="{myScroller.x}" 
        width="{myScroller.width}" 
        height="{myScroller.height}">
        <s:stroke>
            <s:SolidColorStroke color="0x686868" weight="1"/>
        </s:stroke>
    </s:Rect>

    <!--- Defines the appearance of the Button subcomponent. -->
    <s:Button id="modeButton"
        width="150"
        x="0" 
        minHeight="25"/>
                           
    <!--- Defines the appearance of the RichEditableText subcomponent. -->
    <s:Scroller id="myScroller"
        width="200"
        x="156">
        <s:RichEditableText id="textInput" 
            minHeight="25"
            heightInLines="4"
            widthInChars="20"
            paddingLeft="4" paddingTop="4"
            paddingRight="4" paddingBottom="4"/>
    </s:Scroller>
</s:SparkSkin>

Creating the ModalText component

The following code implements the class definition for the ModalText component. The ModalText component is a composite component that contains a Button and a RichEditableText subcomponent:
package myComponents
{
    import flash.events.Event;
    import spark.components.Button;
    import spark.components.RichEditableText;
    import spark.components.supportClasses.SkinnableComponent;

    // ModalText dispatches a change event when the text of the  
    // RichEditableText subcomponent changes. 
    [Event(name="change", type="flash.events.Event")] 
    
    /** a) Extend SkinnableComponent. */ 
    public class ModalText extends SkinnableComponent
    {
        /** b) Implement the constructor. */ 
        public function ModalText() {
            super();
            
            // Set the skinClass style to the name of the skin class.
            setStyle("skinClass", ModalTextSkin);
        }
        
        /** c) Define the skin parts for the Button 
        *     and RichEditableText subcomponents. **/ 
        [SkinPart(required="true")]
        public var modeButton:Button;
        
        [SkinPart(required="true")]
        public var textInput:RichEditableText;
        
        /** d) Implement the commitProperties() method to handle the
        *     change to the ModalText.text property. 
        *     Changes to the ModalText.text property are copied to 
        *     the  RichEditableText subcomponent. */ 
        override protected function commitProperties():void { 
            super.commitProperties(); 
            
            if (bTextChanged) { 
                bTextChanged = false; 
                textInput.text = _text; 
            } 
        }       
        
        /** e) Implement the partAdded() method to
        *     initialize the Button and RichEditableText subcomponents. */ 
        override protected function partAdded(partName:String, instance:Object):void {
            super.partAdded(partName, instance);
        
            if (instance == textInput) {
                textInput.editable = false; 
                textInput.text= _text; 
                textInput.addEventListener("change", textInput_changeHandler); 
            }

            if (instance == modeButton) {
                modeButton.label = "Toggle Editing Mode"; 
                modeButton.addEventListener("click", modeButton_clickHandler); 
            }
        }
        
        /** f) Implement the partRemoved() method to remove the 
        *     event listeners added by partAdded(). */ 
        override protected function partRemoved(partName:String, instance:Object):void {
            super.partRemoved(partName, instance);
        
            if (instance == textInput) {
                textInput.removeEventListener("change", textInput_changeHandler);
            }

            if (instance == modeButton) {
                textInput.removeEventListener("click", modeButton_clickHandler);
            }
        }

        /** g) Add methods, properties, and metadata.
        *     The general pattern for properties is to specify a 
        *     private holder variable. */
        
        // Implement the ModalText.text property. 
        private var _text:String = "ModalText"; 
        private var bTextChanged:Boolean = false; 
        
        // Create a getter/setter pair for the text property. 
        [Bindable] 
        public function set text(t:String):void { 
            _text = t; 
            bTextChanged = true; 
            invalidateProperties(); 
        } 
        
        public function get text():String { 
                return textInput.text; 
        } 
        
        // Dispatch a change event when the RichEditableText.text
        // property changes. 
        private function textInput_changeHandler(eventObj:Event):void { 
                dispatchEvent(new Event("change")); 
        } 
 
        // Handle the Button click event to toggle the 
        // editting mode of the RichEditableText subcomponent. 
        private function modeButton_clickHandler(eventObj:Event):void { 
                textInput.editable = !textInput.editable; 
        } 
    }
}

Creating the ModalTextStates component

The following code example implements the class definition for the ModalTextStates component. The ModalTextStates component modifies the ModalText components shown in the previous section to add view states to the skin class. This control has all of the attributes of the ModalText class, and adds the following attributes:

  • Uses the textPlacement property of the component to make the RichEditableText subcomponent appear on the right side or the left side of the component.

  • Editing the textPlacement property of the control dispatches the placementChanged event.

  • Uses the textPlacement property as the source for a data binding expression.

  • Setting the textPlacement property so that the RichTextArea component appears on the right configures the skin class to use normal view state. Setting it to appear on the left uses the textLeft view state.

  • Disabling editing of the RichTextArea component sets the view state of the skin class to normalDisabled or textLeftDisabled.

The following example uses the ModalTextStates component in an application:
<?xml version="1.0" encoding="utf-8"?>
<!-- asAdvancedSpark/SparkMainModalTextStates.mxml -->
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    xmlns:MyComp="myComponents.*">
     <s:layout>
        <s:VerticalLayout/>
    </s:layout>

    <fx:Script>
      <![CDATA[
        import flash.events.Event;
    
        private function placementChangedListener(event:Event):void {
          myEvent.text="placementChanged event occurred - textPlacement = " 
              + myMT.textPlacement as String;
        }
      ]]>
    </fx:Script>

    <MyComp:ModalTextStates id="myMT" 
        textPlacement="left"
        placementChanged="placementChangedListener(event);"/>
    <s:TextArea id="myEvent" width="50%" height="25"/>    

    <s:Label text="Change Placement"/>
    <s:Button label="Set Text Placement Right" 
        click="myMT.textPlacement='right';" />
    <s:Button label="Set Text Placement Left" 
        click="myMT.textPlacement='left';"/>
</s:Application>

The executing SWF file for the previous example is shown below:

This applications sets the initial placement of the RichEditableText subcomponent to left. Use the buttons to switch the placement between right and left. When the placement changes, the event handler for the placementChanged event displays the current placement.

Creating the skin class for the ModalTextStates component

The skin class for the ModalTextStates component, ModalTextStatesSkin.mxml, defines the following view states to control the display of the component:
  • normal The RichEditableText subcomponent is on the right, and editing is enabled.

  • disabled The RichEditableText subcomponent is on the right, and editing is disabled.

  • textLeft The RichEditableText subcomponent is on the left, and editing is enabled.

  • textLeftDisabled The RichEditableText subcomponent is on the left, and editing is disabled.

Shown below is the skin definition:

<?xml version="1.0" encoding="utf-8"?>
<!-- asAdvancedSpark\myComponents\ModalTextStatesSkin.mxml -->
<s:SparkSkin xmlns:fx="http://ns.adobe.com/mxml/2009" 
    xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:mx="library://ns.adobe.com/flex/mx" 
    minWidth="100" minHeight="25">

    <!-- Define ModalTextState as the host component of the skin. --> 
    <fx:Metadata>
        <![CDATA[ 
            [HostComponent("myComponents.ModalTextStates")]
        ]]>
    </fx:Metadata> 

    <!-- Define the view states. --> 
    <s:states>
        <s:State name="normal" />
        <s:State name="normalDisabled" stateGroups="disabledGroup"/>
        <s:State name="textLeft"/>
        <s:State name="textLeftDisabled" stateGroups="disabledGroup"/>
    </s:states>

    <!-- Define the border around the RichEditableText control. --> 
    <s:Rect x="{myScroller.x}" 
        width="{myScroller.width}" 
        height="{myScroller.height}">
        <s:stroke>
            <s:SolidColorStroke color="0x686868" weight="1"/>
        </s:stroke>
    </s:Rect>

    <!--- Defines the appearance of the Button subcomponent. -->
    <s:Button id="modeButton" 
        width="150"
        x="0" 
        x.textLeft="206" 
        x.textLeftDisabled="206"
        minHeight="25" height="100%"/>
                           
    <!--- Defines the appearance of the RichEditableText subcomponent. -->
    <s:Scroller id="myScroller"
        width="200"
        x="156" 
        x.textLeft="0" 
        x.textLeftDisabled="0">
        <s:RichEditableText id="textInput" 
            alpha="1.0" alpha.disabledGroup="0.5"
            minHeight="25"
            heightInLines="4"
            widthInChars="20" 
            paddingLeft="4" paddingTop="4"
            paddingRight="4" paddingBottom="4"/>
    </s:Scroller>
</s:SparkSkin>

Creating the ModalTextStates component

Shown below is the code for the ModalTextStates component. The ModalTextStates class modifies the ModalText class in the following ways:

  • Adds the implementation of the ModalTextStates() method to set the view state of the skin.

  • Adds the implementation of the textPlacement property to set the placement of the RichEditableText subcomponent.

package myComponents
{
    import flash.events.Event;
    import spark.components.Button;
    import spark.components.RichEditableText;
    import spark.components.supportClasses.SkinnableComponent;

    // ModalText dispatches a change event when the text of the  
    // RichEditableText subcomponent changes, 
    // and a placementChanged event
    // when you change the textPlacement property of ModalText.
    [Event(name="change", type="flash.events.Event")] 
    [Event(name="placementChanged", type="flash.events.Event")]

    // Define the skin states implemented by the skin class.
    [SkinState("normal")]
    [SkinState("normalDisabled")]
    [SkinState("textLeft")]
    [SkinState("textLeftDisabled")]
   
    /** a) Extend SkinnableComponent. */ 
    public class ModalTextStates extends SkinnableComponent
    {
        /** b) Implement the constructor. */ 
        public function ModalTextStates() {
            super();
            
            // Set the skin class.
            setStyle("skinClass", ModalTextStatesSkin);
        }
        
        /** C) Define the skin parts. */ 
        [SkinPart(required="true")]
        public var modeButton:Button;
        
        [SkinPart(required="true")]
        public var textInput:RichEditableText;
        
        /** d) Implement the commitProperties() method. */ 
        override protected function commitProperties():void { 
            super.commitProperties(); 
            
            if (bTextChanged) { 
                bTextChanged = false; 
                textInput.text = _text; 
            } 
        }       
        
        /** e) Implement the partAdded() method. */ 
        override protected function partAdded(partName:String, instance:Object):void {
            super.partAdded(partName, instance);
        
            if (instance == textInput) {
                textInput.editable = false; 
                textInput.text= _text; 
                textInput.addEventListener("change", textInput_changeHandler); 
            }

            if (instance == modeButton) {
                modeButton.label = "Toggle Editing Mode"; 
                modeButton.addEventListener("click", modeButton_changeHandler); 
            }
        }
        
        /** f) Implement the partRemoved() method. */ 
        override protected function partRemoved(partName:String, instance:Object):void {
            super.partRemoved(partName, instance);
        
            if (instance == textInput) {
                textInput.removeEventListener("change", textInput_changeHandler);
            }

            if (instance == modeButton) {
                textInput.removeEventListener("click", modeButton_changeHandler);
            }
        }

        /** g) Implement the getCurrentSkinState() method. 
        *     The view state is determined by the _textPlacement property 
        *     and the editable property of the RichEditableText subcomponent.*/ 
        override protected function getCurrentSkinState():String {
            var returnState:String = "normal";
            
            if (textInput.editable == true) 
            {
                if (_textPlacement == "right") {
                    returnState = "normal"; 
                }
                else if (_textPlacement == "left") {
                    returnState = "textLeft"; 
                }
            }

            if (textInput.editable == false) 
            {
                if (_textPlacement == "right") {
                    returnState = "normalDisabled";
                }
                else if (_textPlacement == "left") {
                    returnState = "textLeftDisabled"; 
                }
            }
            return returnState;
        }

        /** h) Add methods, properties, and metadata.  
        *     The general pattern for properties is to specify a private 
        *     holder variable. */
        
        // Define the text property. 
        private var _text:String = "ModalText"; 
        private var bTextChanged:Boolean = false; 
        
        // Then, create a getter/setter pair for the text property. 
        [Bindable] 
        public function set text(t:String):void { 
            _text = t; 
            bTextChanged = true; 
            invalidateProperties(); 
        } 
        
        public function get text():String { 
            return textInput.text; 
        } 

        // Define the textPlacement property. 
        private var _textPlacement:String = "right";
        
        // Create a getter/setter pair for the textPlacement property.
        // Dispatch the placementChanged event when it changes.
        [Bindable]
        public function set textPlacement(p:String):void {
            _textPlacement = p;
            invalidateSkinState();
            dispatchEvent(new Event("placementChanged"));
        }
        
        public function get textPlacement():String {
            return _textPlacement;
        }
        
        // Dispatch a change event when the RichEditableText.text
        // property changes. 
        private function textInput_changeHandler(eventObj:Event):void { 
            dispatchEvent(new Event("change")); 
        } 
 
        // Handle the Button click event to toggle the 
        // editting mode of the RichEditableText subcomponent. 
        private function modeButton_changeHandler(eventObj:Event):void { 
            textInput.editable = !textInput.editable;
            invalidateSkinState(); 
        } 
    }
}