Null in Flex ComboBox

apache-flexcomboboxnull

How can you make a ComboBox where the user can select null?

If you simply create a combobox with null in the dataprovider, the value appears but the user cannot select it:

<mx:ComboBox id="cb" dataProvider="{[null, 'foo', 'bar']}" />

Is there a way to make that null selectable?

A workaround is to add an item into the dataProvider that is not null but 'represents' null; and then map between null and that object every time you access the combobox. But that's not really an elegant solution; you would always have to keep this mapping in mind in all code that accesses a 'nullable' combobox…

Edit: expanding on why I don't like the workaround:
It could be done in a subclass of course, but either I introduce new attributes (like nullableSelectedItem); but then you have to be careful to always use these attributes. Or I override ComboBoxes selectedItem; but I'm affraid that is going to break the base class: it might not like something changing its idea of what the current selected item is from within. And even this fragile hack works, on top of selectedItem and dataProvider this nullItem then also needs to be handled special in data and listData for renderers, in labelFunction, and then it's probably still being exposed in events the ComboBox sends…
It might work, but it's quite a hack just to fix the problem that if the user clicks on the item it isn't activated (for the rest the ComboBox handles null fine).
(Another alternative is to have a ui component delegate to a ComboBox, but that's even much more code just to avoid this small problem)

Best Answer

It seems that the only way to correctly manage an empty item is to actually add an item to the dataprovider of the combobox. The following subclass will handle this automatically.

The implementation is a little tricky in order to support changes on dataprovider, in terms of items add/remove and of complete reassignment of the dataprovider itself (just think about arraycollections bindings to remote services' response).

package {

    import flash.events.Event;
    import flash.events.FocusEvent;
    import mx.collections.ArrayCollection;
    import mx.collections.ICollectionView;
    import mx.collections.IList;
    import mx.containers.FormItem;
    import mx.controls.ComboBox;
    import mx.events.CollectionEvent;
    import mx.events.ListEvent;
    import mx.validators.Validator;

    public class EmptyItemComboBox extends ComboBox {

        protected var _emptyItem:Object = null;
        protected var _originalDataProvider:ICollectionView = null;

        public function EmptyItemComboBox() {
            super();
            addEmptyItem();
            addEventListener(Event.CHANGE, onChange);
        }

        private function onChange(event:Event):void {
            dispatchEvent(new Event("isEmptySelectedChanged"));
        }

        [Bindable]
        public function get emptyItem():Object {
            return _emptyItem;
        }

        public function set emptyItem(value:Object):void {
            if (_emptyItem != value) {
                clearEmptyItem();
                _emptyItem = value;
                addEmptyItem();
            }
        }

        [Bindable(event="isEmptySelectedChanged")]
        public function get isEmptySelected():Boolean {
                return (selectedItem == null || (_emptyItem != null && selectedItem == _emptyItem));
        }

        override public function set selectedItem(value:Object):void {
            if (value == null && emptyItem != null) {
                super.selectedItem = emptyItem;
            } else    if (value != selectedItem) {
                super.selectedItem = value;
            }
            dispatchEvent(new Event("isEmptySelectedChanged"));
        }

        override public function set dataProvider(value:Object):void {
            if (_originalDataProvider != null) {
                _originalDataProvider.removeEventListener(
                        CollectionEvent.COLLECTION_CHANGE,
                        onOriginalCollectionChange);
            }
            super.dataProvider = value;
            _originalDataProvider = (dataProvider as ICollectionView);
            _originalDataProvider.addEventListener(
                    CollectionEvent.COLLECTION_CHANGE,
                    onOriginalCollectionChange)
            addEmptyItem();
        }

        private function clearEmptyItem():void {
            if (emptyItem != null && dataProvider != null 
                    && dataProvider is IList) {
                var dp:IList = dataProvider as IList;
                var idx:int = dp.getItemIndex(_emptyItem);
                if (idx >=0) {
                    dp.removeItemAt(idx);    
                }
            }
            dispatchEvent(new Event("isEmptySelectedChanged"));
        }

        private function addEmptyItem():void {
            if (emptyItem != null) {
                 if (dataProvider != null && dataProvider is IList) {
                    var dp:IList = dataProvider as IList;
                    var idx:int = dp.getItemIndex(_emptyItem);
                    if (idx == -1) {
                        var newDp:ArrayCollection = new ArrayCollection(dp.toArray().concat());
                        newDp.addItemAt(_emptyItem, 0);
                        super.dataProvider = newDp;
                    }
                }
            }
            dispatchEvent(new Event("isEmptySelectedChanged"));
        }

        private function onOriginalCollectionChange(event:CollectionEvent):void {
            if (_emptyItem != null) {
                dataProvider = _originalDataProvider;
                addEmptyItem();
            }
        }
    }
}

Some notes about the class:

  • It will insert the empty object automatically on the list.. this was a strong requirements for my scenario: the dataproviders was returned by remote services and they couldn't include additional elements just to support the Flex UI, neither I could manually watch every collection to create empty items on each collection refresh.

  • Since it has to work with the contents of the collection, it will internally create a copy of the original one with the same items' instances AND the empty one, so the instance of the original collection will not be touched at all and can be reused in other context.

  • It will listen for changes on the original dataprovider, allowing to work on it and even to assign a competely new one: the empty item will be recreated automatically.

  • You are in control providing the actual instance to use as the "empty item" with the emptyItem attribute: this allows to keep a consistent data type with the rest of the collection (if you use typed objects), or to define a custom label on it.

Some example code using it...

    <mx:Script>
    <![CDATA[
        import mx.controls.Alert;
    ]]>
    </mx:Script>

    <mx:ArrayCollection id="myDP">
    <mx:source>
        <mx:Array>
            <mx:Object value="1" label="First"/>  
            <mx:Object value="2" label="Second"/>
            <mx:Object value="3" label="Third"/>
        </mx:Array>
    </mx:source>
    </mx:ArrayCollection>

    <local:EmptyItemComboBox id="comboBox" dataProvider="{myDP}" labelField="label">
    <local:emptyItem>
        <mx:Object value="{null}" label="(select an item)"/>  
    </local:emptyItem>
    </local:EmptyItemComboBox>

    <mx:Button label="Show selected item" click="Alert.show(comboBox.selectedItem.value)"/>

    <mx:Button label="Clear DP" click="myDP.removeAll()"/>
    <mx:Button label="Add item to DP" click="myDP.addItem({value: '4', label: 'Fourth'})"/>
    <mx:Button label="Reset DP" click="myDP = new ArrayCollection([{value: '1', label: 'First'}])"/>

    <mx:Label text="{'comboBox.isEmptySelected = ' + comboBox.isEmptySelected}"/>

</mx:Application>