I have a custom component made up of a selectable control (radio button)
and a text input. I want to perform some logic in response to the
change events from both of those controls, but after that I want
anything that is registered on the composite component's change handler
to have a change to handle the events as well. The problem is, when I
re-dispatch the events the event target has changed to my custom
component, losing the originating event's target.
Here's my custom component:
<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" label="{listItem.#text}" data="{listItem.#level.toString()}">
<mx:Script>
<![CDATA[
import mx.controls.RadioButtonGroup;
[Bindable]
public var selected: Boolean;
[Bindable]
public var text: String;
[Bindable]
public var listItem: XML;
[Bindable]
public var group: RadioButtonGroup;
private function onSelectionChange(event: Event): void {
selected = event.target.selected;
dispatchEvent(event);
}
private function onTextChange(event: Event): void {
text = event.target.text;
dispatchEvent(event);
}
]]>
</mx:Script>
<mx:RadioButton group="{group}" label="{label}" selected="{selected}" change="onSelectionChange(event)"/>
<mx:TextInput width="100%"
maxChars="{listItem.specify.#entryLength}"
enabled="{selected}"
visible="{listItem.hasOwnProperty('specify')}"
includeInLayout="{visible}"
change="onTextChange(event)"/>
</mx:HBox>
In the event handler that receives change events from this component, I
see that event.target is an instance of SpecifyRadioButton, not the
TextInput or RadioButton, as I'd expect. How should I propagate the
event to get what I want here?
Getting event [Event type="change" bubbles=false cancelable=false eventPhase=2]
from question0.tabSurvey.questionForm.questionContainer.Single94.VBox95.SpecifyRadioButton111
Instead of re-dispatching the original event, create a new event and pass the original event as a origEvent property. The new event which the SpecifyRadioButton dispatches can either be a custom event class which extends Event, or you can be lazy and just use mx.events.DynamicEvent.
Example:
import mx.events.DynamicEvent;
private function onSelectionChange(event: Event): void {
selected = event.target.selected;
var newEvent:DynamicEvent = new DynamicEvent(Event.CHANGE);
newEvent.origEvent = event;
dispatchEvent(newEvent);
}
Then, in your handlers for the SpecifyRadioButton.change event, reference the event.origEvent property.
It makes sense that the target of the event would be SpecifyRadioButton, because that's what is dispatching the event.
The TextInput component's "change" event is set to not bubble up, meaning that it can be listened to by listeners in the same component as it, but nowhere else. If you want the change event to bubble up, you're going to have to extend the TextInput class (or use something nifty like Mate).
package {
import flash.events.Event;
import mx.controls.TextInput;
public class CarbonatedTextInput extends TextInput {
public function CarbonatedTextInput () {
super();
addEventListener(Event.CHANGE, forceBubbles);
}
/*
We have to remove the event listener before we
dispatch the new event (with bubbles!) because
otherwise our listener in the constructor would
catch us and put us in an endless loop...
resulting in a **STACK OVERFLOW**
*/
protected function forceBubbles(e:Event):void {
removeEventListener(Event.CHANGE, forceBubbles);
dispatchEvent(new Event(Event.CHANGE, true));
addEventListener(Event.CHANGE, forceBubbles);
}
}
}
Related
I would like to programmatically change a selected item, in a tree or list, to the item currently "marked/focused" under the mouse pointer .
I'm working with an Flex Air standalone application.
I was thinking in the lines of:
myTree.selectedItem = EVENT.TARGET (where EVENT could be a mouseover/rightclick/rollOver event, and TARGET should be the node/item currently under the mouse pointer).
Is there a way of doing this (or in any other way)?
Ahh, and i want to do it without left clicking ;-)
Thank you in advance,
Sebastian
I found this interesting enough so I am asking if this is the easiest way to achieve this. First off, instead of the list, you need to add the rollOver-listener to the ItemRenderer, not to the list itself (as the event.target and event.currentTarget will just show your list).
So lets create a custom ItemRenderer and add a rollOver listener
<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" height="20" rollOver="itemrenderer1_rollOverHandler(event)">
<fx:Script>
<![CDATA[
protected function itemrenderer1_rollOverHandler(event:MouseEvent):void
{
this.dispatchEvent(new CustomEvent(CustomEvent.SELECT_ITEM, data, true));
}
]]>
<s:Label id="label1" text="{data.label}"/>
</s:ItemRenderer>
You need to somehow get the value of the selected item (which is the data on the itemRenderer) so I created a CustomEvent-class just to do so.
package
{
import flash.events.Event;
public class CustomEvent extends Event
{
public var selectedItem:Object;
public static const SELECT_ITEM:String = "selectItem";
public function CustomEvent(type:String, selectedItem:Object, bubbles:Boolean=false, cancelable:Boolean=false)
{
super(type, bubbles, cancelable);
this.selectedItem = selectedItem;
}
}
}
then I added a eventListener to the main class and set the list.selectedItem property accordingly:
//for the main MXML initializer:
this.addEventListener(CustomEvent.SELECT_ITEM, rollOverChangeSelected);
//and the function:
protected function rollOverChangeSelected(ce:CustomEvent):void{
list.selectedItem = ce.selectedItem;
}
Another way: bindable variable
The list:
s:List id="list" allowMultipleSelection="true" selectionColor="red" rollOverColor="red" itemRenderer="customItemRenderer" selectedItem="{_rollOverSelectedItem}">
The variable and set / get methods:
[Bindable] public var _rollOverSelectedItem:Object;
public function get rollOverSelectedItem():Object
{
return _rollOverSelectedItem;
}
public function set rollOverSelectedItem(value:Object):void
{
_rollOverSelectedItem = value;
}
and the ItemRenderer's rollOver-method:
protected function itemrenderer1_rollOverHandler(event:MouseEvent):void
{
this.parentApplication.rollOverSelectedItem = data;
}
What is the best/proper way?
I have a class in Flex that crates a custom IconItemRenderer by extending the IconItemRenderer base class. I'm using this custom item within a list and listen to mouse press. Depending on the location of the mouse press, I have different options. One of which is to navigate to a different view. I know how to use the change listener of the list to push to a new view but don't want to implement it. The idea for the mouse click is that depending on the location, I can remove elements from the list or open up the current element.
For the life of me I cannot find a method to navigate to a new view from within the IconItemRenderer. This is the code I'm using, both the class and the list where I implement it.
package components
{
import spark.components.Button;
import spark.components.IconItemRenderer;
import spark.utils.MultiDPIBitmapSource;
public class DeleteItemRenderer extends IconItemRenderer
{
private var btn:Button;
private var xIcon:MultiDPIBitmapSource;
public function DeleteItemRenderer()
{
super();
super.iconWidth = super.iconHeight = 40;
super.labelField = 'title';
super.decorator = "assets/delete.png";
}
override public function set data(value:Object):void{
super.data = value;
}
override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void{
setElementPosition(decoratorDisplay, unscaledWidth-40, 5);
setElementSize(decoratorDisplay, 40, 40);
}
override protected function measure():void{
measuredHeight = 50;
}
override protected function drawBackground(unscaledWidth:Number, unscaledHeight:Number):void{
graphics.beginFill(0xffffff);
graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
graphics.endFill();
decoratorDisplay.smooth = true;
graphics.lineStyle(1,0xcccccc,1);
graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
graphics.endFill();
}
}
}
List
<s:List id="survey_list" visible="true" width="98%" height="70%" contentBackgroundColor="#FFFFFF" horizontalCenter="0">
<s:itemRenderer>
<fx:Component>
<components:DeleteItemRenderer width="99.9%" height="98%" verticalAlign="top" click="detectActionPress();">
<fx:Script>
<![CDATA[
import spark.components.ViewNavigator;
import spark.components.View;
import mx.events.FlexEvent;
import mx.core.FlexGlobals;
import mx.core.UIComponent;
import spark.components.List;
private var application:UIComponent = FlexGlobals.topLevelApplication as UIComponent;
private var pressOpen:Number = application.width - 40;
//private var _navigator:ViewNavigator = FlexGlobals.topLevelApplication.navigation; //navigation is not defined uhhhh why????
private function detectActionPress():void{
var localX:Number = this.mouseX;
if(localX <= pressOpen){
engangeElement();
}
else{
deleteElement();
}
}
private function deleteElement():void{
var parentList:List = owner as List;
parentList.dataProvider.removeItemAt(parentList.dataProvider.getItemIndex(data));
trace('element removed');
}
private function engangeElement():void{
var parentList:List = owner as List;
var _test:ViewNavigator = this.parentDocument as ViewNavigator;
//this.parentApplication.navigator.pushView(views.UnfinishedSurvey, parentList.selectedItem.shortcode)
_test.pushView(views.UnfinishedSurvey, parentList.selectedItem.shortcode);
}
]]>
</fx:Script>
</components:DeleteItemRenderer>
</fx:Component>
</s:itemRenderer>
</s:List>
Any ideas how I could push a new view from engageElement();
Why don't I have access to the navigator?
Thanks
Generally; I would recommend dispatching an event from inside the itemRenderer. Be sure that the event bubbles.
You can listen to the event on a class which is a hierarchical parent of the itemRenderer / List.
In the event handler you can access the navigator.
I wrote about the approaches here; which may provide more details.
There are alternate ways to do this. You could use a class that has an instance to the navigator and inject that into your renderer using some type of dependency injenction (DI) framework. Robotlegs and Swiz are two ActionScript based frameworks that support this.
Simple and straight forward.
I extended a mx.controls.TextInput to create a custom component with a different behavior.
I'm trying to set the text property on the keyDownHandler(), and for some reason it doesn't work as I expected. The text on the component just kinda ignore the change.
I'm using Flex 3.6.
Down here is a simple example code that explains what's going on:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:customcomponent="com.test.customcomponent.*">
<customcomponent:TextTest x="20" y="20"/>
</mx:Application>
And below de AS class:
package com.test.customcomponent
{
import flash.events.KeyboardEvent;
import mx.controls.TextInput;
public class TextTest extends TextInput
{
public function TextTest()
{
super();
}
override protected function keyDownHandler(event:KeyboardEvent):void{
text = "lol. It doesn't work";
}
}
}
You need to prevent the default key down event.
override protected function keyDownHandler(event:KeyboardEvent):void{
text = "lol. It doesn't work";
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation()
}
In order to be able to prevent the default handling, you have to have a high enough priority on your handler (which keyDownHandler() does not have). This means you need to register your own method with priority > 0.
You can try like this:
public function MyTextInput() {
addEventListener(KeyboardEvent.KEY_DOWN, yourHandler,
false, EventPriority.BINDING, true);
...
}
private function yourHandler(event : KeyboardEvent) : void {
// stop further handling
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
// do your work here
text = ...;
}
Let me try to explain this the best I can.
I have a component containing a data grid with the following data grid column
<mx:DataGridColumn dataField="email" headerText="Email Address">
<mx:itemRenderer>
<mx:Component>
<mx:VBox width="100%" horizontalScrollPolicy="off" verticalScrollPolicy="off" horizontalAlign="center">
<mx:TextInput id="tiEmailAddress"
width="95%"
text="{data.email}"
invalid="{data.isNotValidEmail(event);}"
valid="{data.isValidEmail(event);}"/>
<mx:EmailValidator id="validatorEmailAddress"
source="{tiEmailAddress}"
property="text"
required="true"
requiredFieldError="Email address is required."/>
</mx:VBox>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
Here's the guts of my value object:
[Bindable(event="contactChange")]
public class ContactVO extends EventDispatcher
{
private var _email:String = "default email";
public var selected:Boolean = false;
public var edited:Boolean = false;
public var isNew:Boolean = false;
public var isValid:Boolean = false;
public function ContactVO()
{ }
public function isNotValidEmail(e:Event):void
{
isValid = false;
email = e.target.text;
}
public function isValidEmail(e:Event):void
{
isValid = true;
email = e.target.text;
}
public function get email():String
{
return _email;
}
public function set email(value:String) : void
{
if (value != _email) {
_email = value;
edited = true;
}
dispatchEvent(new Event("contactChange", true));
}
}
Then back in the component, I have this which gets called at creationComplete
addEventListener("contactChange", processContactChange);
Through the Flex debugger, I can see the addEventListener statement called when the component is created, I can see the event fired from the value object when the validation is performed and the value changes, but processContactChange is never called, so I assume the event is never making it to my component.
Any idea what I've gotten wrong here? Thanks!
[SOLUTION]
The conversation with #Flextras below helped to deepen my understanding of this process and figure out where the disconnect was in my understanding. Basically, I changed the innards of my component's data column entry to the following:
<mx:TextInput id="tiEmailAddress"
width="95%"
text="{data.email}"
invalid="{data.isNotValidEmail(event);}"
valid="{data.isValidEmail(event);}"
creationComplete="{addListener(data)}"/>
<mx:Script>
<![CDATA[
private function addListener(data:Object):void
{
var eventDispatcher:EventDispatcher = data as EventDispatcher;
eventDispatcher.addEventListener("contactChange", outerDocument.processContactChange);
}
]]>
</mx:Script>
and removed the event listener from my creationComplete method
When you add the event listener you have to add it to the class that fires the event. In your code, you're adding it to the component that contains the DataGrid. Neither the itemRenderer, DataGrid, nor component containing the DataGrid fire the event.
IF the component containing the DataGrid has access to the ContactVO event, you can listen directly on that.
Otherwise, you can add an event listener in your itemRenderer.
If you absolutely need to execute code in the component containing the DataGrid, then the itemRenderer should listen on the Value Object for the event, then fire an event of it's own. Make sure that the 'itemRenderer' event bubbles; and it will move up the display hierarchy.
Can't seem to bind to data from within a custom component. I've tried BindUtilis and {} but can't seem to fathom it out. Here's what I've got:
I have a class DataModel which has been made bindable
Within Mainn.mxml I have two components: DataGrid (used for testing) & CustomComponent (which extends Canvas)
When the data within DataModel.somelist is updated the DataGrid reflects the changes but the CustomComponent doesn't appear to.
I was expecting to see the trace (CustomComponent.dataProvider) fired whenever this._dataModel.itemList is changed. What am I doing wrong?
Main.mxml looks something like this:
<mx:Script>
<![CDATA[
import model.DataModel;
[Bindable]
private var _dataModel:DataModel = DataModel.getInstance();
]]>
</mx:Script>
<mx:VBox width="100%" height="100%">
<views:ItemDisplayList width="100%" height="300" id="idl" >
<views:dataProvider>
{this._dataModel.itemList}
</views:dataProvider>
</views:ItemDisplayList>
<mx:DataGrid id="dg" width="100%" height="300" >
<mx:dataProvider>
{this._dataModel.itemList}
</mx:dataProvider>
</mx:DataGrid>
</mx:VBox>
The CustomComponent has this AS class:
package code{
import model.DataModel;
import mx.containers.Canvas;
public class CustomComponent extends Canvas{
[Bindable]
private var _dataModel:DataModel = DataModel.getInstance();
private var _dataProvider:ArrayCollection ;
public function CustomComponent(){
super();
_dataProvider = new ArrayCollection();
trace("CustomComponent start");
}
public function get dataProvider() : ArrayCollection {
trace("get dataProvider");
return _dataProvider;
}
public function set dataProvider(value: ArrayCollection) : void {
trace("set dataProvider");
this._dataProvider = value;
invalidateProperties();
invalidateSize();
invalidateDisplayList();
dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE));
}
...
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number ) : void{
trace("updateDisplayList");
super.updateDisplayList(unscaledWidth, unscaledHeight);
}
}
}
Are you resetting the dataProvider in your custom component, or by updating you mean adding/removing an item from dataModel.itemList?
If you're adding an item to dataModel.itemList, then your custom component wont update but the DataGrid will. This is because in the DataGrid (maybe it's in the core ListBase component in Flex?), when you set dataProvider, it adds an event listener for CollectionEvent.COLLECTION_CHANGE. When it hears that (when something in your ArrayCollection/IList changes, from remove/add/refresh/etc), it runs a few methods to update the DataGrid's itemRenderers. You'll have to code that manually if you create your own dataProvider property.
If you set the dataProvider explicitly on your custom component (custom.dataProvider = myArrayCollection), it will update. But that's not the most efficient thing to do. I recommend extending one of the List classes that has already implemented a dataProvider property, it's a lot to tackle.
Check out the source for the mx ListBase's dataProvider method to see what they did.
Hope that helps,
Lance