add ItemRenderer on the fly on Component - apache-flex

I have a List which has TextInput as itemRenderers for all its items. Upon application launch the items are rendered in the TextInputs correctly. The data is being populated from an Array of Objects.
What I want is, after the data has been populated in the ItemRenderers, I want to have an additional item renderer (TextInput of course)...so that if the user wants to enter another item, he can put it in the additional textInput.
And I also want to add the additional itemRenderer each time the user has added a new item and taps ENTER on the newly added item.
Below is my itemRenderer, there is the clearTxt_enterHandler handler..but I wonder how to add another itemRenderer upon "Enter".
Can somebody guide me with this?
Thx
<?xml version="1.0" encoding="utf-8"?>
<s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
autoDrawBackground="true" xmlns:components="components.*" width="100%">
<s:layout>
<s:HorizontalLayout/>
</s:layout>
<fx:Script>http://stackoverflow.com/questions/4199373/flex-4-is-a-good-practice-store-the-loaded-bitmapdata-in-a-value-object-and-then
<![CDATA[
import mx.events.FlexEvent;
import skins.ClearableTextInputSkin;
override public function set data( value:Object ) : void {
super.data = value;
//clearTxt.text = value.label;
}
protected function clearTxt_enterHandler(event:FlexEvent):void
{
trace("On Enter");
}
]]>
</fx:Script>
<components:ClearableTextInput text="{data.label}" id="clearTxt"
skinClass="skins.ClearableTextInputSkin" enter="clearTxt_enterHandler(event)" left="10" top="36" width="220" />
</s:ItemRenderer>
And this is my list that comes from the main application:
<s:List id="myList" itemRenderer="renderers.TextInputRenderer" dataProvider="{xxx}" width="100%">
<s:layout>
<s:TileLayout requestedRowCount="2"columnAlign="justifyUsingWidth"/>
</s:layout>
</s:List>

From the ItemRenderer, dispatch an Event to request the new ItemRenderer. Be sure to make it bubbling, so that it goes all the way up to the List that owns the ItemRenderer.
protected function clearTxt_enterHandler(event:FlexEvent):void
{
dispatchEvent(new Event("myCustomRequestEvent", true));
//replace with a real custom Event; this is for brevity
}
Listen for that event on the List component and in the handler just add a new element to its dataprovider (probably with an empty label).
myList.addEventListener("myCustomRequestEvent", addRow);
private function addRow(event:Event):void {
myList.dataProvider.addItem({label: null});
//replace anonymous object with your class
}
This will add a new item to the List. In your case you may have to bind requestRowCount to the number of items in the List, so that it will grow when you add the items.
<s:VerticalLayout requestedRowCount="{myList.dataprovider.length}" />
BTW: why are you using TileLayout? If what you want is two input boxes per item, you should put two TextInputs in one ItemRenderer and use VerticalLayout. It will make your life a lot easier.

Related

Handling mouse click in Flex 4 List to find the selected item (since itemClick is gone)

I have prepared a simplified test case for my question. It will run instantly in your Flash Builder if you put the 2 files below into a project.
I'm trying to display a List of strings and a confirmation checkbox in a popup:
In the real application I dispatch a custom event with the string selected in the list, but in the test code below I just call trace(str);
My problem: if I use click event, then the window closes, even if I click at a scrollbar (the !str check below doesn't help, when an item had been selected in previous use). And if I use change event, then the window doesn't close, when I click on the same item as the last time. And the itemClick event seems not to be present in spark.components.List anymore.
Any suggestions please on how to handle this probably frequent problem?
Writing a custom item renderer and having a click event handler for each item seems to be overkill for this case, because I have strings in the list.
Test.mxml: (please click myBtn few times - to see my problems with click and change)
<?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="400" minHeight="300">
<fx:Script>
<![CDATA[
import mx.managers.PopUpManager;
private var _popup:Popup = new Popup();
private function showPopup(event:MouseEvent):void {
PopUpManager.addPopUp(_popup, this, true);
PopUpManager.centerPopUp(_popup);
}
]]>
</fx:Script>
<s:Button id="myBtn" right="5" bottom="5"
label="Open window" click="showPopup(event)" />
</s:Application>
Popup.mxml:
<?xml version="1.0" encoding="utf-8"?>
<s:TitleWindow
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
width="240" height="240"
creationComplete="init(event)"
close="close()">
<fx:Script>
<![CDATA[
import mx.collections.ArrayList;
import mx.controls.Alert;
import mx.events.FlexEvent;
import mx.events.CloseEvent;
import mx.events.ItemClickEvent;
import mx.managers.PopUpManager;
private var myData:ArrayList = new ArrayList();
private function init(event:FlexEvent):void {
// XXX in the real app data is updated from server
myData.removeAll();
for (var i:uint = 1; i <= 10; i++)
myData.addItem('Item #' + i);
}
public function close(event:TimerEvent=null):void {
PopUpManager.removePopUp(this);
}
private function handleClick(event:MouseEvent):void {
var str:String = myList.selectedItem as String;
if (!str)
return;
if (myBox.selected) {
Alert.show(
'Select ' + str + '?',
null,
mx.controls.Alert.YES | mx.controls.Alert.NO,
null,
handleConfirm,
null,
mx.controls.Alert.NO
);
} else {
sendEvent();
}
}
private function handleConfirm(event:CloseEvent):void {
if (event.detail == mx.controls.Alert.YES)
sendEvent();
}
private function sendEvent():void {
close();
// XXX in the real app dispatchEvent() is called
trace('selected: ' + (myList.selectedItem as String));
}
]]>
</fx:Script>
<s:VGroup paddingLeft="20" paddingTop="20"
paddingRight="20" paddingBottom="20" gap="20"
width="100%" height="100%">
<s:List id="myList" dataProvider="{myData}"
click="handleClick(event)"
width="100%" height="100%" fontSize="24" />
<s:CheckBox id="myBox" label="Confirm" />
</s:VGroup>
</s:TitleWindow>
Also I wonder, why do I get the warning above:
Data binding will not be able to detect assignments to "myData".
The Spark List dispatches an 'IndexChangeEvent.CHANGE'. You can listen for this event to know when the selection in the List has changed.
<s:List id="myList" dataProvider="{myData}"
change="handleIndexChange()"
width="100%" height="100%" fontSize="24" />
That event is only dispatched whenever the selected index actually changes, which means that when you reopen the window a second time an item might still be selected and when you click on that one, no CHANGE event will be fired. To fix this just deselect the selection before you close the window:
public function close():void {
myList.selectedIndex = -1;
PopUpManager.removePopUp(this);
}
Also make sure to dispatch your event with the selected item before you close the window (and deselect it).
As for your question about the binding warning: you get that message because you didn't mark 'myData' to be bindable. To fix this just use the [Bindable] tag:
[Bindable]
private var myData:ArrayList = new ArrayList();
or skip the binding altogether if you don't need it and just assign the dataprovider to the list in ActionScript:
myList.dataProvider = myData;
I'd recommend two solutions if you absolutely wnat to display what item was selected. Otherwise, the solution provided by RIAStar would do the trick.
Listen to rendererAdd and rendererRemove events within your PopUp
As explained here, you can easily access to your list's renderers without interfering with its virtualLayout business.
Use a custom renderer
I know. But as long as you keep your code clean, itemRenderers won't blow up your application's memory. They're made to render huge amount of items without memory leaks.
In your Test.mxml, modify the codes like:
<fx:Script>
<![CDATA[
import mx.managers.PopUpManager;
private var _popup:Popup;
private function showPopup(event:MouseEvent):void {
_popup = new Popup();
PopUpManager.addPopUp(_popup, this, true);
PopUpManager.centerPopUp(_popup);
}
]]>
</fx:Script>
And in your Popup.mxml, I am not sure why you have the TimerEvent in the close function.
Also the trace won't be shown, as you are calling the close() function immediately after the alert's YES button has been clicked..

Get variable from ItemRenderer to Main Application

I have a List with TextInput as item renderer. I want to get the value entered in the TextInput (form the TextInputItemRenderer) and pass it the main application to do some checks(upon tapping enter on the textInput -- see code below).
I know that we can do it thru dispatching event but I still don't understand how to pass a variable from the ItemRenderer to the main app.
Help Pls.
Thanks
<?xml version="1.0" encoding="utf-8"?>
<s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
autoDrawBackground="true" xmlns:components="components.*" width="100%"
>
<s:layout>
<s:HorizontalLayout/>
</s:layout>
<fx:Script>
<![CDATA[
override public function set data( value:Object ) : void {
super.data = value;
}
protected function myTextInput_enterHandler(event:FlexEvent):void
{
trace(myTextInput.text);
What Next??
}
]]>
</fx:Script>
<components:ClearableTextInput text="{data.label}" id="myTextInput" enter="myTextInput_enterHandler(event)"/>
</s:ItemRenderer>
i'm not sure if I got your question correctly but would this help?
http://www.ajibanda.blogspot.com/2011/02/changing-currentstate-of-main-and.html
Instead of trying to access from MainApp to itemRenderer, i think you can do backward. Follow one of two solutions below:
In itemRenderer, assign value you want to check later to a public global variable on the MainApp. The limitation is you then only enable to check it on MainApp, not any where esle (other itemRenderer, component, module etc.)
Use EvenBus to put the value to a global container. Create a static eventBus instance in AppUtils, for example. In itemRenderer, AppUtils.eventBus.dispatch() an event with the value attached to it each time the value changed. And then use AppUtils.eventBus again to addEventListener() to retrieve the value and check wherever you want. Google AS3Commons for EventBus.

in Flex DropDownList, is there a way to bind to a property of an item in dataProvider?

I have following code.
<s:DropDownList dataProvider={_dataProvider}/>
<fx:Script>
private var _dataProvider:ArrayCollection = new ArrayCollection([{label:"one", data:1}, {label:"two", data:2}]);
</fx:Script>
I want to bind the data property of the selectedItem in the DropDownList. Is there a way to do this?
Not sure if this another approach to the question... but I created a custom dropdownlist and binded the selectedItem to incoming changes. When the value of my desired data changes it will trigger the dropdownlist to change its selection.
DropDownListBindable.mxml
<?xml version="1.0" encoding="utf-8"?>
<s:DropDownList xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx">
<fx:Script>
<![CDATA[
import mx.controls.Alert;
[Bindable] public var valueField:String = "";
override public function set selectedItem(value:*):void{
try{
for(var i:uint=0;i<this.dataProvider.length;i++){
if(this.dataProvider[i][this.valueField]==value){
this.selectedIndex=i;
break;
}else{
this.selectedIndex=-1;
}
}
}catch(e:Error){}
}
]]>
</fx:Script>
</s:DropDownList>
On the application I import the custom dropdownlist and bind the valuefield with whatever needs to be binded... in your case its 'data'. I also created an object called 'mydata' for the dropdownlist to listen to for changes. When mydata changes the list will too. I've added a button to demonstrate how the list changes.
main.mxml
<?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"
xmlns:components="com.components.*">
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
[Bindable] private var myData:Object = new Object();
[Bindable] private var _dataProvider:ArrayCollection = new ArrayCollection([{label:"one", data:1}, {label:"two", data:2}]);
]]>
</fx:Script>
<s:HGroup>
<components:DropDownListBindable
dataProvider="{_dataProvider}"
prompt="--select one--"
selectedItem="{this.myData}"
id="ddl"
valueField="data"
labelField="label"/>
<s:Button label="change datafield" click="this.myData=1"/>
</s:HGroup>
</s:Application>
I'm pretty sure the answer is no, but just to be clear; I'm confused.
If your dataProvider contains objects like this:
{label:"one", data:1}
First off, this syntax will create a generic object with no customization. If none of the properties on that object are explicitly defined, none of them can implement the Bindable metadata tag, and therefore when used as the source for data binding, the target will never update.
Second off, even if you created your own non-generic object with properties being bindable, binding doesn't usually go multiple levels deep into an object's properties of an array.
The selectedItem will point to an object like are in your _dataProvider, or possibly null, based on user interaction with the dropDownList. Binding the selectedItem to a property inside the item doesn't make sense; because you'd be comparing an literal to an object and nothing would ever be selected.
I'm unclear, without looking, what happens in the DropDownList when you try to set selectedItem to an item not in your dataProvider. I imagine it resets the selection, though.
If you can expand on what exactly you're trying to accomplish we may be able to help more.
<s:DropDownList id="ddl" dataProvider="{_dataProvider}"/>
<s:Label text="{ddl.selectedItem.data.toString()}"/>
Yes. You can do this. It is quite simple, actually. I do it all the time:
<s:DropDownList dataProvider="{_dataProvider}" selectedItem="#{_selectedItem}" />
With ActionScript that looks like this:
private var _dataProvider:ArrayCollection = new ArrayCollection([{label:"one", data:1}, {label:"two", data:2}]);
[Bindable] private var _selectedItem;
Every time the user selects an item in the drop down list, the _selectedItem will get set.

Flex DataGrid with ComboBox itemRenderer

I'm going spare trying to figure out the "correct" way to embed a ComboBox inside a Flex (3.4) DataGrid. By Rights (e.g. according to this page http://blog.flexmonkeypatches.com/2008/02/18/simple-datagrid-combobox-as-item-editor-example/) it should be easy, but I can't for the life of me make this work.
The difference I have to the example linked above is that my display value (what the user sees) is different to the id value I want to select on and store in my data provider.
So what I have is:
<mx:DataGridColumn headerText="Type" width="200" dataField="TransactionTypeID" editorDataField="value" textAlign="center" editable="true" rendererIsEditor="true">
<mx:itemRenderer>
<mx:Component>
<mx:ComboBox dataProvider="{parentDocument.transactionTypesData}"/>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
Where transactionTypesData has both 'data' and 'label' fields (as per what the ComboBox - why on earth it doesn't provide both a labelField and idField I'll never know).
Anyway, the above MXML code doesn't work in two ways:
The combo box does not show up with any selected item.
After selecting an item, it does not store back that selected item to the datastore.
So, has anyone got a similar situation working?
While Jeff's answer is a partial answer for one approach for this (see http://flex.gunua.com/?p=119 for a complete example of this being used to good effect), it isn't as general as I wanted.
Thankfully, I finally found some great help on Experts Exchange (the answers by hobbit72) describes how to create a custom component that works in a grid as a ItemRenderer.
I've extended that code to also support using the combo box as an ItemEditor as well. The full component is as follows:
<?xml version="1.0" encoding="utf-8"?>
<mx:ComboBox
xmlns:mx="http://www.adobe.com/2006/mxml"
dataChange="setSelected()"
change="onSelectionChange(event)"
focusEnabled="true">
<mx:Script>
<![CDATA[
import mx.events.DataGridEvent;
import mx.events.ListEvent;
import mx.controls.dataGridClasses.DataGridListData;
private var _ownerData:Object;
private var _lookupField:String = "value";
// When using this component as an itemEditor rather than an itemRenderer
// then set ' editorDataField="selectedItemKey"' on the column to
// ensure that changes to the ComboBox are propogated.
[Bindable] public var selectedItemKey:Object;
public function set lookupField (value:String) : void {
if(value) {
_lookupField = value;
setSelected();
}
}
override public function set data (value:Object) : void {
if(value) {
_ownerData = value;
setSelected();
}
}
override public function get data() : Object {
return _ownerData;
}
private function setSelected() : void {
if (dataProvider && _ownerData) {
var col:DataGridListData = DataGridListData(listData);
for each (var dp:Object in dataProvider) {
if (dp[_lookupField] == _ownerData[col.dataField]) {
selectedItem = dp;
selectedItemKey = _ownerData[col.dataField];
return;
}
}
}
selectedItem = null;
}
private function onSelectionChange (e:ListEvent) : void {
if (selectedItem && _ownerData) {
var col:DataGridListData = DataGridListData(listData);
_ownerData[col.dataField] = selectedItem[_lookupField];
selectedItemKey = selectedItem[_lookupField];
}
}
]]>
</mx:Script>
</mx:ComboBox>
Using this component is straight forward. As an ItemRenderer:
<mx:DataGridColumn headerText="Child" dataField="PersonID" editable="false" textAlign="center">
<mx:itemRenderer>
<mx:Component>
<fx:GridComboBox dataProvider="{parentDocument.childrenData}" labelField="Name" lookupField="PersonID" change="dispatchEvent(new mx.events.DataGridEvent(mx.events.DataGridEvent.ITEM_FOCUS_OUT, true, true))"/>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
Using this component is straight forward. And as an ItemEditor:
<mx:DataGridColumn labelFunction="lookupChildName" headerText="Child" dataField="PersonID" editable="true" editorDataField="selectedItemKey">
<mx:itemEditor>
<mx:Component>
<fx:GridComboBox dataProvider="{parentDocument.childrenData}" labelField="Name" lookupField="PersonID" change="dispatchEvent(new mx.events.DataGridEvent(mx.events.DataGridEvent.ITEM_FOCUS_OUT, true, true))"/>
</mx:Component>
</mx:itemEditor>
</mx:DataGridColumn>
Note that when using it as an ItemEditor, a custom labelFunction (that looks up the Name from the PersonID in my case) must be used, otherwise you only see the key in the grid when the field isn't being edited (not a problem if your keys/values are the same).
Note that in my case, I wanted the item focus out event to propogate up to provide immediate feedback to the user (my DataGrid has itemFocusOut="handleChange()"), hence the change event creating an ITEM_FOCUS_OUT event.
Note that there are probably simpler ways to have a ComboBox as an ItemEditor when you don't mind the ComboBox only shown when the user clicks on the cell to edit. The approach I wanted was a generic way to show a combo box in a DataGrid for all rows, and being editable and with decent event propogation.
The easiest way to add itemRenderers to DataGrids is to make a custom MXML component. In your case make a canvas, HBox, or VBox as the custom component and add the combobox as a child.Set the dataProvider on the dataGrid itself and assign the itemRenderer to the column, and then override the set data function of the itemRenderer to access all data from the given data provider for that instance as seen below:
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
override public function set data(value:Object):void{
trace(value.data);
trace(value.name);
}
]]>
</mx:Script>
<mx:ComboBox width="100%" height="100%" id="myComboBox"/>
</mx:HBox>
This method will be called for each instance of the itemRenderer
In my case I used a spark datagrid where one of the columns has an ItemRenderer that utilises a DropDownListBox. My problem was that when my item list change, the DropDownLists doesn't get updated with the new dataProvider. To solve this, I had to pass the dataProvider for the DropDownListBox as part of the data (of the ItemRenderer), and then by overriding the setter of the data to just assign the DropDownlListBox's dataProvider. Probably a bit of overhead, but if someone have a better solution, please let me know:
<s:GridItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx">
<fx:Script>
<![CDATA[
override public function set data(v : Object) : void {
super.data = v;
if (v == null)
return;
dropDown.dataProvider = data.dataProvider;
}
]]>
</fx:Script>
<s:DropDownList id="dropDown" width="100%" height="100%" dataProvider="{data.dataProvider}" labelField="name"/>

Background image that shows when list is empty in Flex

How would I display a backgroundImage on a List when the List is empty?
At the moment the list is populated when items are dropped inside after a drag-and-drop but I would prefer a solution that checks for any change to the data to determine if the list is empty.
The List inherits a backgroundImage from its ScrollControlBase but what would be the best way to make it appear when the list is empty and disappear when an item is added.
Any suggestions?
Thanks!
In the past, I've done it with states for a component. Quick and dirty example would be something like this in your custom component:
<mx:List currentState="{(listItemsDataProvider.length > 0) ? 'HasItemsState' : 'NoItemsState'}">
// anything else you need
</mx:List>
and of course creating those states in the component, with the NoItemsStates changing the background image, or if your component is a container, like a Canvas, then you can have the state not display the List at all.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
[Bindable] public var listItems:ArrayCollection = new ArrayCollection();
private function removeAllItemsFromList():void
{
this.listItems.removeAll();
backgroundCheck()
}
private function addItemToList():void
{
this.listItems.addItem({data:null,label:"test"});
backgroundCheck()
}
private function backgroundCheck():void
{
if(this.listItems.length>0)
{
this.myList.setStyle("backgroundImage", null)
}
else
{
this.myList.setStyle("backgroundImage", "me.png")
}
}
]]>
</mx:Script>
<mx:VBox width="100%" height="100%">
<mx:List id="myList" width="100%" height="100%" backgroundImage="me.png" dataProvider="{this.listItems}"/>
<mx:HBox width="100%">
<mx:Button id="addItemButton" click="addItemToList()" label="add item"/>
<mx:Button id="removeItemsButton" click="removeAllItemsFromList()" label="remove all items"/>
</mx:HBox>
</mx:VBox>
</mx:Application>
This is how I would approach it, checking the dataProvider length. In your case you'd do so when the drop is complete.
You could extend the List control and override updateDisplayList(). Draw the backgroundImage if dataProvider.length == 0 else call super.updateDisplayList() to get normal List behavior. This will make the new List control easy to reuse if you need to.
Use the same property, set the image to null when you have some data. You may take a look at custom ItemRenderers as well.

Resources