I have a custom component ExpandCollapseMenu that extends SkinnableContainer. This component can have state "normal" or "expanded".
Inside this component I have buttons, with different skin based on ExpandCollapseMenu's state.
This works fine when defining the buttons inside ExpandCollapsMenu's skin class:
<s:Group id="contentGroup" top="20" left="10" right="10" bottom="10">
<s:layout>
<s:VerticalLayout/>
</s:layout>
<component:ExpandCollapseMenuButton label="Foo" skinClass.expanded="component.ExpandCollapseMenuButtonExpandedSkin" skinClass.normal="component.ExpandCollapseMenuButtonSkin" />
</s:Group>
But I don't want to define the buttons inside ExpandCollapsMenu's skin class, I want to define them where I use ExpandCollapseMenu. Like this:
<component:ExpandCollapseMenu skinClass="component.ExpandCollapseMenuSkin">
<component:ExpandCollapseMenuButton label="Foo" />
</component:ExpandCollapseMenu>
At this level, I can't reference skinclass.expanded, but I got it working by using CSS like this:
component|ExpandCollapseMenu:expanded component|ExpandCollapseMenuButton {
skinClass: ClassReference("component.ExpandCollapseMenuButtonExpandedSkin");
}
component|ExpandCollapseMenu:normal component|ExpandCollapseMenuButton {
skinClass: ClassReference("component.ExpandCollapseMenuButtonSkin");
}
Is this a good way to change skin based on parent containers state? Or is there a better way?
I recommend you to solve the problem on a ExpandCollapseMenu container level. When ExpandCollapseMenu's state is changed you should iterate over children and set some flag for each of them.
So I suggest to introduce expanded:Boolean flag for ExpandCollapseMenuButton.
The problem occurs if ExpandCollapseMenu can contain other instance's types. In this case we can solve it using the following two ways:
Check components in process of iterating if they are ExpandCollapseMenuButton instances (using is operator).
Introduce simple Expandable interface like the following:
public interface Expandable
{
function set expanded(value:Boolean);
}
and implement this interface by ExpandCollapseMenuButton. So you can use is operator in iterator body more flexible way without ExpandCollapseMenuButton dependency.
So the last part of puzzle is to implement setter in ExpandCollapseMenuButton class and switch skin:
private var expandedDirty:Boolean;
private var _expanded:Boolean;
public function set expanded(value:Boolean)
{
if (value == _expanded)
return;
_expanded = value;
expandedDirty = true;
invalidateProperties();
}
override protected function commitProperties():void
{
super.commitProperties();
if (expandedDirty)
{
if (_expanded)
setStyle("skinClass", ExpandCollapseMenuButtonExpandedSkin);
else
setStyle("skinClass", ExpandCollapseMenuButtonSkin);
expandedDirty = false;
}
}
Related
I am trying to embed an image using an item renderer in my Flex project.
The image path however is a String passed in as a bound variable.
I am aware that
<s:BitmapImage source="#Embed('/../assets/image.png')" />
works because the image is embedded at runtime? (Could someone please clarify this)
How would i go about embedding my bound string, somewhat like this:
<s:BitmapImage source="#Embed('/../assets/{data.image}')" />
Many Thanks
I think a better choice if you'd like to embed the image but find it dynamically at runtime is: Embed all of the images it could be and then grab a reference to it dynamically. We generally use a pattern like this:
public class Icons {
[Embed(source="icons/icon1.png")]
public var icon1:Class;
[Embed(source="icons/icon2.png")]
public var icon2:Class;
}
Then you can dynamically grab the embedded images from your Icons instance at run-time.
Edit - self contained example - I'll use an item renderer since I think that's what you're doing.
Let's assume data.image can be 'plane' 'train' or 'automobile'
<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">
<fx:Script>
<![CDATA[
[Embed(source="/assets/icons/plane.png")]
public var plane : Class;
[Embed(source="/assets/icons/train.png")]
public var train : Class;
[Embed(source="/assets/icons/automobile.png")]
public var automobile : Class;
]]>
</fx:Script>
<s:Image source="{this[data.image]}"/>
</s:ItemRenderer>
This is a really simple example and not the BEST way to implement, but you get the idea.
I like embed icons with css files. Then in ItemRenderer you can set css class and get image which you want.
css file or mxml css block:
.icons
{
bender: Embed(source="/assets/bender.png");
/* other icons */
}
In renderer, when you override set data method:
override public function set data(value:Object):void
{
super.data = value;
var iconName:String = data.image;
if ( iconName )
{
var cssDecl2:CSSStyleDeclaration = styleManager.getStyleDeclaration(".icons");
var IconClass:Class = cssDecl2.getStyle( iconName );
bmImage.source = new IconClass();
}
}
and bmImage as id s:BitmapImage:
<s:BitmapImage id="bmImage" />
I want to put a textinput box on the header of a datagrid in flex, whose values changes dynamically according to the no of data or value present in that perticular column.
Please any one help me, am posting the image acctually what I want.
Thanks in advance.
I'll assume you can use Spark DataGrid. In that case you just need to create a custom headerRenderer. To keep things simple we'll start from the default headerRenderer. In your Flex SDK sources, find the class spark.skins.spark.DefaultGridHeaderRenderer. Copy this file into your project and rename it appropriately.
Inside this class, find the components labelDisplayGroup and sortIndicatorGroup (they're at the bottom). They're inside an HGroup, so we can simply add our counter component in between.
<!-- I removed the original comments for brevity -->
<s:HGroup left="7" right="7" top="5" bottom="5" gap="2" verticalAlign="middle">
<s:Group id="labelDisplayGroup" width="100%" />
<!-- our counter component -->
<s:Label id="numRowsDisplay" />
<s:Group id="sortIndicatorGroup" includeInLayout="false" />
</s:HGroup>
So far for the visual component; now we have to update its text property appropriately. In the script block add the following snippet:
private var dp:IList;
override public function set owner(value:DisplayObjectContainer):void {
if (dp) dp.removeEventListener(CollectionEvent.COLLECTION_CHANGE, updateNumRows);
if (super.owner) super.owner.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onPropertyChange);
super.owner = value;
dp = value ? DataGrid(value).dataProvider : null;
updateNumRows();
if (dp) dp.addEventListener(CollectionEvent.COLLECTION_CHANGE, updateNumRows);
if (value) value.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onPropertyChange);
}
private function onPropertyChange(event:PropertyChangeEvent):void {
if (event.property == 'dataProvider') {
dp = event.newValue as IList;
updateNumRows();
}
}
private function updateNumRows(event:CollectionEvent=null):void {
numRowsDisplay.text = (dp ? dp.length : 0) + "";
}
What happens here? the owner property of the renderer refers to the data component that holds this renderer; in this case a DataGrid. So when the owner is assigned to the renderer, we access its dataProvider and use its length to update the counter component.
So what are those listeners for? There are two cases you might want to foresee.
The number of items in the dataprovider changes: that's why we listen on the dataprovider for CollectionChange events.
The entire dataProvider is changed or nullified: for this we listen for PropertyChangeEvents on the DataGrid and update the counter only when the 'dataProvider' property changes
As I am somewhat new to Flex I may be missing something fundamental here. I have a Spark List container whose dataProvider is bound to a result set coming back from a RemoteObject call. Pretty standard stuff.
<s:List id="list" dataProvider="{model.stuff}" width="100%" height="100%"
selectedIndex="#{selectedSlider.value}"
itemRenderer="{stuffRenderer}">
</s:List>
The selectedIndex is associated with an HSlider, but that is not the problem. My issue is that I would like to automatically select a certain "preferred" element from the list (only initially...to guide the user).
I tried to do that in a creationComplete event but my data hadn't shown up yet...setting selectedIndex didn't work...it was too early.
What's the right way to do this?
private function findAllUsers_resultHandler(e:ResultEvent):void
{
list.dataProvider = new ArrayCollection(e.result as Array);
if(firstTry)
{
list.selectedIndex = 0;
firstTry = false;
}
}
spark.components.List has spark.components.SkinnableDataContainer in its class hierarchy which dispatches a dataProviderChanged event whenever the dataProvider changes. Unfortunatly there is no [Event] metadata in SkinnableDataContainer that allows using this event in MXML. So, you'll need to create your own custom component that extends List.
package
{
import spark.components.List;
[Event(name="dataProviderChanged", type="flash.events.Event")]
public class MyList extends List
{
public function MyList()
{
super();
}
}
}
By using your custom component you can add an event listener for dataProviderChanged and update your selectedIndex accordingly.
<ns1:MyList id="list" dataProvider="{model.stuff}" width="100%" height="100%"
dataProviderChanged="selectedIndex = selectedSlider.value"
selectedIndex="#{selectedSlider.value}"
itemRenderer="{stuffRenderer}">
</ns1:MyList>
BTW: This works with other List-based components (like DropDownList) too.
I believe it should work if you just set the initial value of the slider to the index you want to be selected at the beginning.
Something like this:
<s:List dataProvider="{yourData}" selectedIndex="{hSlider.value}" /> <s:HSlider id="hSlider" minimum="0" maximum="{yourData.length - 1}" stepSize="1" value="theIndexYouWantAsInitial" liveDragging="true" />
That should work.
HTH
FTQuest
Say I have the following custom component:
<s:Group 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[
[Bindable]
public var prop:String;
private function formatProp() : String {
return "Hello, " + prop;
}
]]>
</fx:Script>
<s:Label text="User: {prop}"/>
<s:Label text="Greeting: {formatProp()}"/>
</s:Group>
If I add it to my application like this:
<local:MyComponent prop="Hello"/>
The result looks like:
User: Mark
Greeting: Hello, null
It seems Flex is setting prop on my custom component after it has already initialized the child labels, so it's reliant on the property changed event to set the user label.
Is there an elegant way to make Flex wait for all of my component's properties to be set before initially evaluating bindings?
Note: I realize the formatProp function is trivial and could be included inline, but this is just a simplified example.
The "elegant way" would be to actually provide data binding, so that you can change your property also afterwards. Your initial idea looked good, working with the answer provided by Cornel. I just wanted to mention this as your actual question sounded more like that you know your data binding is not working and you just wanted to postpone the initial setting of the variable.
Btw, should you plan to create custom components in Actionscript (instead of mxml) you'll face the opposite problem: properties are set before you had a chance to actually create your children, so you may need to buffer them if they actually should influence some childs properties.
it is not related to component livecycle, more to binding rules. Your function "formatProp" should recieve the parameter "prop" as a parameter in order to be called when the prop is changed. Try this code:
private function formatProp(props:String) : String {
return "Hello, " + props;
}
<s:Label text="Greeting: {formatProp(prop)}"/>
I am trying to define the content area of a custom component that extends mx:VBox. The component has predefined headers and footers (visual children) and I want to set the area in the middle for adding children to the component. The component will be used like this:
<custom_component>
<mx:button/>
</custom_component>
How would I set this content area?
There's actually a few steps to it.
Your custom component needs to set its DefaultProperty metadata so the children don't collide with the ones within the custom component itself.
Then you need to stow them away in an instance var to add to your content area later, because the properties will be set before the components children are created.
Lastly, if multiple children are specified your DefaultProperty will be handed an Array object (rather than a single UIComponent instance.)
So your custom component would look something like this:
<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300">
<mx:Metadata>
[DefaultProperty("content")]
</mx:Metadata>
<mx:HBox id="headerBox"/>
<mx:VBox id="contentBox"/>
<mx:HBox id="footerBox"/>
<mx:Script>
<![CDATA[
import mx.core.UIComponent;
private var _contentChildren:Array;
public function set content(c:*) : void {
// Allow 1 or more children to be specified
_contentChildren = (c as Array) || [c];
}
override protected function createChildren() : void {
// Call super so contentBox gets created first
super.createChildren();
for each (var child:UIComponent in _contentChildren) {
contentBox.addChild(child);
}
}
]]>
</mx:Script>
</mx:VBox>
In your custom component, add the DefaultProperty metadata tag:
[DefaultProperty("nameOfDefaultProperty")]
Then you would also define a setter for that property:
public function set nameOfDefaultProperty(value:UIComponent):void
{
if (value != null)
{
// add "value" to the display list here
}
}
Try using a canvas?