Search This Blog

Sunday, 8 September 2013

Right Align the drop down of the Flex DropDownList/ComboBox

In the previous post, I have tried to play around with the width of the drop down popup of the Flex DropDownList. An interesting observation is that the drop down is always aligned to the left edge of the open button of the DropDownList. Now it could be a requirement to open it aligned to the right edge or even centered with respect to the open button. So how to achieve that?

For this we need to dig into the DropDownListSkin. We can see that the component opens the drop down popup using a PopUpAnchor which is aligned with left = 0. Now this causes, the popup to stick to the left edge. So, in order to right align it we need to clear the left constraint and make right = 0. For clearing the left constraint, all that we have to do is to set it to undefined

Now, it works fine if we have fixed width for the drop down but in a previous post, we have seen that we can have the width of the drop down to auto expand as well. This auto expansion breaks our alignment and the popup does not remain aligned to the right edge.

A simple solution that I have employed is to keep track of the popup width by using Binding and whenever the width changes, I just re-position the popup. The same method can be used to position the popup in the center. All you have to do is to calculate the left/right constraint position from where the popup needs to open up.

A sample application showing both the auto expanding and the fixed width DropDownList is shown below. Three buttons are provided to change the alignment of the drop down on the fly. You can also see the behavior of the lists when they are placed near the edges of the screen as well.


Since the drop down uses PopUpAnchor, it has a built in functionality which does not allow the popup to go out of bounds of the screen and that works perfectly well for us!

So here is my CustomDropDownList, which can be used to position the drop down to either align with the left edge, right edge or be centrally aligned.



package com.codedebugged.dropdown.alignment
{
    import mx.binding.utils.BindingUtils;
    import mx.binding.utils.ChangeWatcher;
    import mx.events.FlexEvent;
    import spark.components.DropDownList;
    import spark.components.PopUpAnchor;

    public class CustomDropDownList extends DropDownList
    {

        public static const JUSTIFIED : String = "JUSTIFIED";

        public static const LEFT_EDGE_ALIGNED : String = "LEFT_EDGE_ALIGNED";

        public static const RIGHT_EDGE_ALIGNED : String = "RIGHT_EDGE_ALIGNED";

        public function CustomDropDownList()
        {
            super();
            setStyle( "skinClass", CustomDropDownListSkin );
        }

        [SkinPart]
        public var popUp : PopUpAnchor;

        private var _changeWatcher : ChangeWatcher;

        private var _dropDownWidth : Number = NaN;

        private var _popupPosition : String = LEFT_EDGE_ALIGNED;

        private var dropDownWidthChanged : Boolean;

        private var popupPositionChanged : Boolean;

        [Bindable]
        public function get dropDownWidth() : Number
        {
            return _dropDownWidth;
        }

        public function set dropDownWidth( value : Number ) : void
        {
            if (_dropDownWidth != value)
            {
                _dropDownWidth = value;
                dropDownWidthChanged = true;
                invalidateProperties();
            }
        }

        [Inspectable( enumeration = "LEFT_EDGE_ALIGNED,JUSTIFIED,RIGHT_EDGE_ALIGNED" )]
        [Bindable]
        public function get popupPosition() : String
        {
            return _popupPosition;
        }

        public function set popupPosition( value : String ) : void
        {
            if (_popupPosition != value)
            {
                _popupPosition = value;
                popupPositionChanged = true;
                invalidateDisplayList();
            }
        }

        override protected function commitProperties() : void
        {
            super.commitProperties();

            if (dropDownWidthChanged)
            {
                dropDownWidthChanged = false;
                setDropDownWidth();
            }
        }

        override protected function partAdded( partName : String, instance : Object ) : void
        {
            super.partAdded( partName, instance );

            if (instance == popUp)
            {
                popUp.addEventListener( FlexEvent.CREATION_COMPLETE, handlePopupCreation );
            }
            else if (instance == dropDown)
            {
                setDropDownWidth();
            }
        }

        override protected function partRemoved( partName : String, instance : Object ) : void
        {
            super.partRemoved( partName, instance );

            if (instance == popUp)
            {
                popUp.removeEventListener( FlexEvent.CREATION_COMPLETE, handlePopupCreation );
                if (_changeWatcher)
                {
                    _changeWatcher.unwatch();
                    _changeWatcher = null;
                }
            }
        }

        override protected function updateDisplayList( unscaledWidth : Number, unscaledHeight : Number ) : void
        {
            super.updateDisplayList( unscaledWidth, unscaledHeight );

            if (popupPositionChanged)
            {
                popupPositionChanged = false;
                setPopupPosition();
            }
        }

        private function handlePopupCreation( event : FlexEvent ) : void
        {
            setPopupPosition();
        }

        private function justifyPopup() : void
        {
            if (popUp && popUp.popUp)
            {
                popUp.left = (this.width - popUp.popUp.width) / 2;
            }
        }

        private function leftifyPopup() : void
        {
            if (popUp && popUp.popUp)
            {
                popUp.left = (this.width - popUp.popUp.width);
            }
        }

        private function refreshPopup( obj : Object = null ) : void
        {
            if (popUp)
            {
                if (popupPosition == JUSTIFIED)
                {
                    justifyPopup();
                }
                else if (popupPosition == RIGHT_EDGE_ALIGNED)
                {
                    leftifyPopup();
                }
                popUp.updatePopUpTransform();
            }
        }

        private function setDropDownWidth() : void
        {
            if (dropDown && !isNaN( dropDownWidth ))
            {
                dropDown.width = dropDownWidth;
            }
        }

        private function setPopupPosition( object : Object = null ) : void
        {
            if (popUp)
            {
                if (!_changeWatcher)
                {
                    _changeWatcher = BindingUtils.bindSetter( refreshPopup, popUp, [ "popUp", "width" ], false, true );
                }

                if (popupPosition == LEFT_EDGE_ALIGNED)
                {
                    popUp.right = 0;
                    popUp.left = 0;
                }
                else if (popupPosition == JUSTIFIED)
                {
                    justifyPopup();
                    popUp.right = undefined;
                }
                else if (popupPosition == RIGHT_EDGE_ALIGNED)
                {
                    leftifyPopup();
                    popUp.right = undefined;
                }
            }
        }
    }
}


A small modification is required to the skin as well for which I have created my CustomDropDownListSkin which has this minor modification :


    
<s:PopUpAnchor id="popUp"
                   displayPopUp.normal="false"
                   displayPopUp.open="true"
                   includeIn="open"
                   bottom="0"
                   popUpPosition="below"
                   itemDestructionPolicy="never"
                   top="{height}"
                   popUpWidthMatchesAnchorWidth="false">


The component provides a popupPosition property which can be set to LEFT_EDGE_ALIGNEDRIGHT_EDGE_ALIGNED or JUSTIFIED depending on the your requirement. Easy as that!

The same trick can be used to achieve any kind of custom alignment as well. All you have to figure out is the starting position of your popup and position the popupAnchor to it.

Download the source code.

Sunday, 1 September 2013

How to specify the width of the drop down popup in a Flex DropDownList

In a previous post, it has been seen that it is quite easy to make the drop down of the DropDownList auto expand to the size of the contents. But we have also seen that it is a bit flaky, in the sense, the width of the popup jumps around when it encounters content that is longer than any it has been displaying previously. This is because the drop down uses the DataGroup which creates itemRenderers judicially on a "as needed" basis. In some cases, it might be better if the popup opens up to a fixed width to give a consistent UI performance. Again, the standard DropDownList does not allow for it and we have to make a simple tweak to make this happen.

The DropDownList which essentially extends from SkinnableDataContainer, defines a optional SkinPart - dropDown which defines the drop down list area  to be displayed when the list is open. If you have a look at the DropDownListSkin, we can see that this SkinPart is very much present and is actually the wrapper for our drop down contents. So all we need to do is to give it a fixed width and our work is done.

This can be achieved easily by extending the current DropDownList and using a property which can be used to specify the width of the drop down. The simple solution would look something like below:

package com.codedebugged.dropdown.fixedwidth
{
    import spark.components.DropDownList;
    import spark.components.PopUpAnchor;

    public class AutoExpandOrFixedWidthDropDownList extends DropDownList
    {

        [SkinPart]
        public var popUp : PopUpAnchor;

        private var _dropDownWidth : Number = NaN;

        private var dropDownWidthChanged : Boolean;

        [Bindable]
        public function get dropDownWidth() : Number
        {
            return _dropDownWidth;
        }

        public function set dropDownWidth( value : Number ) : void
        {
            if (_dropDownWidth != value)
            {
                _dropDownWidth = value;
                dropDownWidthChanged = true;
                invalidateProperties();
            }
        }

        override protected function commitProperties() : void
        {
            super.commitProperties();

            if (dropDownWidthChanged)
            {
                dropDownWidthChanged = false;
                setDropDownWidth();
            }
        }

        override protected function partAdded( partName : String, instance : Object ) : void
        {
            super.partAdded( partName, instance );

            if (instance == popUp)
            {
                popUp.popUpWidthMatchesAnchorWidth = false;
            }
            else if (instance == dropDown)
            {
                setDropDownWidth();
            }
        }

        private function setDropDownWidth() : void
        {
            if (dropDown && !isNaN( dropDownWidth ))
            {
                dropDown.width = dropDownWidth;
            }
        }
    }

}


The component provides a simple property - dropDownWidth which can be set by the user and it sets the width of the drop down to it. If this property is not set, the list behaves like an auto expanding one as seen in the previous post. A sample application to show the working of the list can be seen below.



Sometimes, it might be difficult to guess the exact width of the drop down but you would still want it open up to a fixed width encompassing even your largest element in the dataprovider. This is where you can use the typicalItem property of the DropDownList. The List based controls use this property to guess their initial width. If you don't specify it, it takes the first item of the dataProvider. You can use it to your benefit by iterating through the dataProvider and finding your largest content. Specify this item as the typicalItem and it would calculate its size depending on it. I will try to post an example of it later.

The same patch should be applicable for a ComboBox as well.

Still, I think the list can be bettered. As you would have noticed, the drop down always opens up aligned to left edge. There could be a need to open the drop down aligned to the right edge or open it up so that is centered to the open button of the DropDownList. I will take this up in another post.

Download the source code

Saturday, 31 August 2013

Auto Expand the width of drop down popup of Flex Spark DropDownList / ComboBox

I am pretty sure many people would have run into this kind of requirement many times. The problem is that when using the Spark controls like the DropDownList, the actual drop down that displays the list of options is defaulted to the width of the DropDownList component. If the length of options are greater than the width of the component, then a horizontal scroll bar appears automatically. Now, horizontal scroll bars are a bit of pain and never look good on a GUI. However, there is a very easy way in which the DropDownList can be made to auto expand to the length of the options it displays.

The key lies in the skin of the DropDownList - DropDownListSkin which uses a PopUpAnchor to control the opening and closing of the drop down options. Here in lies the problem. By default, it sets the popUpWidthMatchesAnchorWidth=true on the PopUpAnchor. To make the DropDownList auto expanding, all that needs to be done is to create a new skin which would be an exact copy of the DropDownListSkin and just set the popUpWidthMatchesAnchorWidth=false as shown below:

    <s:PopUpAnchor id="popUp"  displayPopUp.normal="false" displayPopUp.open="true" includeIn="open"
        left="0" right="0" top="0" bottom="0" itemDestructionPolicy="auto"
        popUpPosition="below" popUpWidthMatchesAnchorWidth="false">


As easy as that! Another way of doing it would be through ActionScipt. For this, you need to create a new component extending from the DropDownList and set the popUpWidthMatchesAnchorWidth false in the new component. The source is shown below:

package com.codedebugged.dropdown.autoexpand
{
    import spark.components.DropDownList;
    import spark.components.PopUpAnchor;

    public class AutoExpandDropDownList extends DropDownList
    {

        [SkinPart]
        public var popUp : PopUpAnchor;

        override protected function partAdded( partName : String, instance : Object ) : void
        {
            super.partAdded( partName, instance );

            if (instance == popUp)
            {
                popUp.popUpWidthMatchesAnchorWidth = false;
            }
        }
    }
}



A sample application to show the difference between the normal DropDownList and the auto expanding one :



Notice that the drop down of the list on the right automatically grows. However, the auto expansion is a bit flaky as it jumps around in size when it encounters a lengthier data in the dataprovider. A better way would be to possibly give the drop down a fixed width which I will try to look into in another post. Since the ComboBox and the DropDownList basically work on the same principle, the same trick can be applied to get a auto expanding ComboBox as well.

Download the source

Saturday, 24 August 2013

Flex Popup as a UIComponent

One of the things that I don't like about the Flex Popup's is that despite it being a "view", I can not declare it within mxml. I have to create a separate class for the popup component and then use the PopUpManager API to display it and remove it. Wouldn't be so nice if I could just declare the component to opened up, within my mxml  and I could easily pass/get data from the component using bindings etc.

It is especially useful when creating Skinnable components as I can declare some component as a SkinPart and in the skin declare it to be opened up in a popup.

As I was looking to do something like that, I came across another Flex component - PopUpAnchor.
This is a very useful component which can be used to position a popup control. What this control doesn't do, is to open the popup in modal and centered manner. So I decided to create my own PopUpUIComponent component by stripping this PopUpAnchor of all the positioning code and use it only to open centered/ modal popups.

I quickly hatched a sample application which allows user to input text and then the text is passed onto the popup using bindings. The application can be seen below.


The PopUpUIComponent looks like this:


package
{
    import flash.display.DisplayObject;
    import flash.events.Event;
    import mx.core.FlexGlobals;
    import mx.core.IFlexDisplayObject;
    import mx.core.IUIComponent;
    import mx.core.UIComponent;
    import mx.managers.PopUpManager;
    import mx.styles.ISimpleStyleClient;

    [DefaultProperty( "popUp" )]
    public class PopUpUIComponent extends UIComponent
    {

        public function PopUpUIComponent()
        {
            addEventListener( Event.ADDED_TO_STAGE, addedToStageHandler );
            addEventListener( Event.REMOVED_FROM_STAGE, removedFromStageHandler );
        }

        [Bindable]
        public var isCentered : Boolean = true;

        [Bindable]
        public var isModal : Boolean = true;

        [Bindable]
        public var parentOfPopup : DisplayObject = FlexGlobals.topLevelApplication as DisplayObject;

        private var _displayPopUp : Boolean = false;

        private var _popUp : IFlexDisplayObject;

        public function get displayPopUp() : Boolean
        {
            return _displayPopUp;
        }

        /**
         *  If <code>true</code>, adds the <code>popUp</code> control to the PopUpManager.
         *  If <code>false</code>, it removes the control.
         *
         *  @default false
         */
        public function set displayPopUp( value : Boolean ) : void
        {
            if (_displayPopUp == value)
            {
                return;
            }

            _displayPopUp = value;
            addOrRemovePopUp();
        }

        public function get popUp() : IFlexDisplayObject
        {
            return _popUp
        }

        [Bindable( "popUpChanged" )]

        /**
         *  The IFlexDisplayObject to add to the PopUpManager when the PopupUIComponent is opened.
         *  If the <code>popUp</code> control implements IFocusManagerContainer, the
         *  <code>popUp</code> control will have its
         *  own FocusManager. If the user uses the Tab key to navigate between
         *  controls, only the controls in the <code>popUp</code> control are accessed.
         */
        public function set popUp( value : IFlexDisplayObject ) : void
        {
            if (_popUp == value)
            {
                return;
            }

            _popUp = value;

            if (_popUp is ISimpleStyleClient)
            {
                ISimpleStyleClient( _popUp ).styleName = this;
            }

            dispatchEvent( new Event( "popUpChanged" ));
        }

        private function addOrRemovePopUp() : void
        {
            if (popUp == null)
            {
                return;
            }

            if (DisplayObject( popUp ).parent == null && displayPopUp)
            {
                PopUpManager.addPopUp( popUp, parentOfPopup, isModal );
                if (isCentered)
                {
                    PopUpManager.centerPopUp( popUp );
                }
                if (popUp is IUIComponent)
                {
                    IUIComponent( popUp ).owner = this;
                }
            }
            else if (DisplayObject( popUp ).parent != null && displayPopUp == false)
            {
                removeAndResetPopUp();
            }
        }

        private function addedToStageHandler( event : Event ) : void
        {
            addOrRemovePopUp();
        }

        private function removeAndResetPopUp() : void
        {
            PopUpManager.removePopUp( popUp );
        }

        private function removedFromStageHandler( event : Event ) : void
        {
            if (popUp != null && DisplayObject( popUp ).parent != null)
            {
                removeAndResetPopUp();
            }
        }
    }

}

The opening and the closing of the popup is controlled by displayPopup var (same as in PopUpAnchor).

It is not the most perfect component but it does make things easier for me to use popups in mxml skins.Of course, there are some good alternatives out there if you are using frameworks like Cairngorm which has a nice popup library.

Hope it helps!

Download the source code

Flex Sort.compareFunction and ListCollectionView.getItemIndex trap

I have run into this peculiar problem twice now and to be fair I should have been careful the second time around. But of course my memory failed me and I ended up committing the same mistake again (stupid me :( ). So I decided to document it and hope others don't end making the same error.

To illustrate my case, let me paste a simple application that can be run locally.


<?xml version="1.0" encoding="utf-8"?>
<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"
               minWidth="955"
               minHeight="600"
               creationComplete="application1_creationCompleteHandler(event)">

    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;
            import mx.events.FlexEvent;
            import spark.collections.Sort;
            import spark.events.IndexChangeEvent;

            private var srcArray : Array = [ "Black", "Yellow", "Red", "Green", "White" ];

            [Bindable]
            private var srcCollection : ArrayCollection;

            protected function application1_creationCompleteHandler( event : FlexEvent ) : void
            {
                srcCollection = new ArrayCollection( srcArray );
                var sort : Sort = new Sort();
                sort.compareFunction = colorSort;
                srcCollection.sort = sort;
                srcCollection.refresh();
            }

            protected function combobox1_changeHandler( event : IndexChangeEvent ) : void
            {
                if (comboBox.selectedItem)
                {
                    slctdLbl.text = srcCollection.getItemIndex( comboBox.selectedItem ).toString();
                }
            }

            private function colorSort( a : Object, b : Object, fields : Array = null ) : int
            {
                if (a == "Black")
                {
                    return 1;
                }
                if (b == "Black")
                {
                    return -1;
                }

                return 0;
            }
        ]]>
    </fx:Script>

    <s:VGroup gap="20">
        <s:ComboBox id="comboBox"
                    width="100"
                    change="combobox1_changeHandler(event)"
                    dataProvider="{srcCollection}"/>
        <s:Label id="slctdLbl"/>
    </s:VGroup>

</s:Application>


It is a simple application which creates a collection of colors and gives it to combobox. When you select any item in the combobox, it displays the index of that item in the collection. The only special thing required here is that I need a special order for the colors in which I want the "Black" color to be at the bottom always. So I do the normal thing , create a Sort object and assign to it my special colorSort function and refresh the collection. All good and done. 

I now fire up the application and select different colors and see the index in the collection. 


The Problem:

When I select the color "Black", it gives me index of -1!!!
Which means that Black can not be found in my collection. But how is this possible?? As can be seen in the code above, Black is definitely part of my collection and also is displayed in the combobox. So what has gone wrong?

The problem actually lies in my colorSort method. For some strange reason, Flex actually uses the compareFunction of the Sort to find items in the collection as well! Since my colorSort does not return 0 for Black color ever, the getItemIndex returns -1 for it.

The ASDoc on the compareFunction of mx.collections.Sort says :

This function must return the following value:
  • -1, if the Object a should appear before the Object b in the sorted sequence
  • 0, if the Object a equals the Object b
  • 1, if the Object a should appear after the Object b in the sorted sequence 
This looks a bit strange to me. The values -1 and 1 refer to a sorted sequence. But the value of 0 talks about equality of the objects not just in terms of equality of precedence in the sort order.
This can be very confusing to people as well as very easy to overlook as I have done in the past. Also the consequences might never be noticed in a testing phase and can actually occur straightaway in the production environment. For me, this a very dangerous situation and this needs to be fixed by the Apache Flex team.

In my view, sorting and fetching are two very different things and they should be kept seperate from each other. There is a flex bug also raised for the same - FLEX-22649

The Solution:

The solution for this issue is very simple. All you have to do is to add the following snippet to the compareFunction at the beginning:


                if (a == b)

                {
                    return 0;
                }


Although it sounds silly, that every sortCompare Function in the world needs to have this piece of code. But atleast, you can be sure that your application will not throw up surprises on just a simple call to getItemIndex.

Hope this post helps someone out there.