hasseg.org

Collapsible Panel Component for Flex

Filed under ActionScript 3, Flex, Programming

Here's an another post in the same vein as my previous one: this time, the component I'm sharing is a Panel subclass that allows for collapsing and expanding its contents. What this means is that the user can click on the header of the Panel to make it toggle between an open or closed state, with a smooth animation.

I Tried to implement the component so that it'll be easy to use in both MXML and ActionScript. In the demo app there is also an example on styling the CollapsiblePanel.

CollapsiblePanelDemo.mxml:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application 
	xmlns:mx="http://www.adobe.com/2006/mxml"
	xmlns:hassegContainers="org.hasseg.containers.*"
	width="100%" height="100%"
	layout="vertical"
	creationComplete="ccHandler(event);"
>
	
	<mx:Style>
		.myCPStyle {
			borderThicknessLeft: 0;
			borderThicknessTop: 0;
			borderThicknessBottom: 0;
			borderThicknessRight: 0;
			headerHeight: 20;
			dropShadowEnabled: false;
			headerColors: #d9d9d9, #ffffff;
			borderAlpha: 1;
			backgroundAlpha: 0;
			paddingTop: 4;
			paddingLeft: 4;
			paddingRight: 4;
			verticalGap: 4;
			
			openIcon: Embed(source="icons.swf", symbol="CollapsiblePanelOpenIcon");
			closedIcon: Embed(source="icons.swf", symbol="CollapsiblePanelClosedIcon");
		}
	</mx:Style>
	
	<mx:Script>
		<![CDATA[
			
			import mx.events.*;
			
			private function ccHandler(event:FlexEvent):void {
				/*
				* An example of programmatically adding a new CollapsiblePanel:
				*/
				var newCP:CollapsiblePanel = new CollapsiblePanel(false); // closed by default
				newCP.title = "This added via AS";
				newCP.styleName = "myCPStyle";
				var bOne:Button = new Button();
				var bTwo:Button = new Button();
				bOne.label = "button one"
				bTwo.label = "button two"
				newCP.addChild(bOne);
				newCP.addChild(bTwo);
				var newLabel:Label = new Label();
				newLabel.text = "Added via ActionScript:";
				this.addChild(newLabel);
				this.addChild(newCP);
			}
			
		]]>
	</mx:Script>
	
	
	<mx:HBox>
		<mx:Box>
			<mx:Label text="styled, open=false:" />
			<hassegContainers:CollapsiblePanel
								title="This is the title"
								width="200"
								open="false"
								styleName="myCPStyle"
			>
				<mx:Button label="hello" />
				<mx:Button label="hello again" />
			</hassegContainers:CollapsiblePanel>
		</mx:Box>
		
		<mx:Box>
			<mx:Label text="styled, open=true:" />
			<hassegContainers:CollapsiblePanel
								title="This is the title"
								width="200"
								open="true"
								styleName="myCPStyle"
			>
				<mx:Button label="hello" />
				<mx:Button label="hello again" />
			</hassegContainers:CollapsiblePanel>
		</mx:Box>
	</mx:HBox>
	
	<mx:HBox>
		<mx:Box>
			<mx:Label text="styled, open attribute not set:" />
			<hassegContainers:CollapsiblePanel
								title="This is the title"
								width="200"
								styleName="myCPStyle"
			>
				<mx:Button label="hello" />
				<mx:Button label="hello again" />
			</hassegContainers:CollapsiblePanel>
		</mx:Box>
		
		<mx:Box>
			<mx:Label text="not styled, open=false:" />
			<hassegContainers:CollapsiblePanel
								title="This is the title"
								width="200"
								open="false"
			>
				<mx:Button label="hello" />
				<mx:Button label="hello again" />
			</hassegContainers:CollapsiblePanel>
		</mx:Box>
	</mx:HBox>
	
	
</mx:Application>

CollapsiblePanel.as:

/*

The MIT License

Copyright (c) 2007-2008 Ali Rantakari of hasseg.org

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*/

package org.hasseg.containers {
	
	import flash.events.*;
	import mx.effects.AnimateProperty;
	import mx.events.*;
	import mx.containers.Panel;
	import mx.core.ScrollPolicy;
	
	
	/**
	* The icon designating a "closed" state
	*/
	[Style(name="closedIcon", property="closedIcon", type="Object")]
	
	/**
	* The icon designating an "open" state
	*/
	[Style(name="openIcon", property="openIcon", type="Object")]
	
	/**
	* This is a Panel that can be collapsed and expanded by clicking on the header.
	* 
	* @author Ali Rantakari
	*/
	public class CollapsiblePanel extends Panel {
		
		private var _creationComplete:Boolean = false;
		private var _open:Boolean = true;
		private var _openAnim:AnimateProperty;
		
		
		
		/**
		* Constructor
		* 
		*/
		public function CollapsiblePanel(aOpen:Boolean = true):void
		{
			super();
			open = aOpen;
			this.addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler);
		}
		
		
		
		
		
		
		
		
		// BEGIN: event handlers				------------------------------------------------------------
		
		private function creationCompleteHandler(event:FlexEvent):void
		{
			this.horizontalScrollPolicy = ScrollPolicy.OFF;
			this.verticalScrollPolicy = ScrollPolicy.OFF;
			
			_openAnim = new AnimateProperty(this);
			_openAnim.duration = 300;
			_openAnim.property = "height";
			
			titleBar.addEventListener(MouseEvent.CLICK, headerClickHandler);
			
			_creationComplete = true;
		}
		
		private function headerClickHandler(event:MouseEvent):void { toggleOpen(); }
		
		private function callUpdateOpenOnCreationComplete(event:FlexEvent):void { updateOpen(); }
		
		// --end--: event handlers			- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		
		
		
		
		
		
		
		
		// BEGIN: private methods				------------------------------------------------------------
		
		// sets the height of the component without animation, based
		// on the _open variable
		private function updateOpen():void
		{
			if (!_open) height = closedHeight;
			else height = openHeight;
			setTitleIcon();
		}
		
		// the height that the component should be when open
		private function get openHeight():Number {
			return measuredHeight;
		}
		
		// the height that the component should be when closed
		private function get closedHeight():Number {
			var hh:Number = getStyle("headerHeight");
			if (hh <= 0 || isNaN(hh)) hh = titleBar.height;
			return hh;
		}
		
		// sets the correct title icon
		private function setTitleIcon():void
		{
			if (!_open) this.titleIcon = getStyle("closedIcon");
			else this.titleIcon = getStyle("openIcon");
		}
		
		// --end--: private methods			- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		
		
		
		
		
		
		
		
		// BEGIN: public methods				------------------------------------------------------------
		
		
		
		/**
		* Collapses / expands this block (with animation)
		*/
		public function toggleOpen():void 
		{
			if (_creationComplete && !_openAnim.isPlaying) {
				
				_openAnim.fromValue = _openAnim.target.height;
				if (!_open) {
					_openAnim.toValue = openHeight;
					_open = true;
					dispatchEvent(new Event(Event.OPEN));
				}else{
					_openAnim.toValue = _openAnim.target.closedHeight;
					_open = false;
					dispatchEvent(new Event(Event.CLOSE));
				}
				setTitleIcon();
				_openAnim.play();
				
			}
			
		}
		
		
		/**
		* Whether the block is in a expanded (open) state or not
		*/
		public function get open():Boolean {
			return _open;
		}
		/**
		* @private
		*/
		public function set open(aValue:Boolean):void {
			_open = aValue;
			if (_creationComplete) updateOpen();
			else this.addEventListener(FlexEvent.CREATION_COMPLETE, callUpdateOpenOnCreationComplete, false, 0, true);
		}
		
		
		/**
		* @private
		*/
		override public function invalidateSize():void {
			super.invalidateSize();
			if (_creationComplete)
				if (_open && !_openAnim.isPlaying) this.height = openHeight;
		}
		
		
		// --end--: public methods			- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		
		
	}

}

25 Comments

Pratap January 25, 2008 at 8:56 AM

Great job,

Wroking very nice in my APP. Am trying to upgrade this component. Trying to resize the panel on mouse move. Can any one help in this. Thanks & Regards PRatap

Greg June 12, 2008 at 11:30 PM

Hi,

First, thanks for the collapsible panel.

I am having problems using it with child components who’s size changes. The ColapsablePanel does not auto resize when the child size changes. In testing using the base Panel it does. I am new to ActionScript and Flex and am not sure what event or method I need to override in order to inherit the parent Panels functionality and am not sure why it does not already.

Do you know why this it does not resize?

Any help much appreciated.

Thanks,

Greg

Gabriela Trindade Perry: HCI, Ergonomia, Flex » Blog Archive » Draggable collapsible container July 13, 2008 at 1:11 AM

[…] So, you can see the result here. View source enabled, of course. It is pretty simple, in fact. Ah! check also the great link by haseg: Collapsible Panel Component for Flex […]

van007 July 27, 2008 at 11:23 AM

Good one but panel dont expand to height given

for example height=“500” dont work

Pete July 29, 2008 at 5:05 AM

Hi, After the collapsible component is created, I tried to dynamically add stuff to the panel upon event triggering. But I am not able to see anything that I add to the panel using addChild(). Why?

Ali Rantakari July 29, 2008 at 2:48 PM

Hi guys, sorry for taking so long to reply – I was on my summer holiday for a while there.

@ Greg:

You should probably look at the documentation for the invalidateSize() and measure() methods in the UIComponent class. I Think this behaviour is set in the overridden invalidateSize() method in this code that I’ve posted, so try commenting that out and go from there (that’s what it looks like from glancing at the code just now – sorry I don’t remember exactly what I did when I coded it and don’t have time to start testing things out now).

@ van007:

See above – I think it’s the same culprit.

@Pete:

Hmm, that’s weird, since I have lots of code that does that with this component and it’s always worked fine. Are you sure the panel is open?

wic November 7, 2008 at 3:40 PM

Regarding sizing.

I figured out a way to do it by setting height to NaN. Add an effect end handler in the constructor.

// In the constructor:			_openAnim.addEventListener(EffectEvent.EFFECT_END, effectEndHandler);

		/** 
		 * If the effect ended in open state, we set height to NaN. This makes flex handle any 
		 * further height calculations automatically. Otherwise the height value will be 
		 * set by the tween effect, which results in an absolut size (ie adding/removing 
		 * components wont resize the panel)  
		 */

		private function effectEndHandler(event:EffectEvent):void
		{
			if (_open) {
				this.explicitHeight = NaN;
				this.invalidateSize();
			}
		}
Un panel collapsable « Flexandgo’s Blog November 12, 2008 at 2:33 PM

[…] ://hasseg.org/blog/post/113/collapsible-panel-component-for-flex/ […]

Phil February 3, 2009 at 2:06 AM

I am using your component. Overall it works well. I do notice, if I set a specific height value I loose my panels. If I remove the height value my panels are displayed again. My work around for now is to set the height of my custom component that sits inside the the panel.

Also, do you know how to set the border color around the panel header. I have been trying with no luck at all. I want a blue line above and below my panel header and nothing on the sides.

Geert April 8, 2009 at 6:41 PM

Hi Ali,

I’m using your component and I’m rather pleased with it. But recently I had an issue with it. I use it this way:

  1. Initially it’s closed.
  2. Initially it contains a label telling it is getting the data
  3. Whenever it is opened for the first time I add extra content. I experienced that sometimes I couldn’t see the content because it’s height didn’t change accordingly. After quite some searching I found out the reason is this: in the method toggleOpen() you’re dispatching the OPEN/CLOSE events too early - before the open animation is done. I changed that method this way and my problem is gone:
/**
* Collapses / expands this block (with animation)
*/
public function toggleOpen():void 
{
	if (_creationComplete &amp;&amp; !_openAnim.isPlaying) {
		_openAnim.fromValue = _openAnim.target.height;
		_openAnim.toValue = _open ? _openAnim.target.closedHeight : openHeight;
		_openAnim.addEventListener(EffectEvent.EFFECT_END, function onEffectEnd():void {
			// Geert (8-Apr-2009)
			// Only adapt the status & title + send the event when the animation has done playing:
			_open = !_open;
			setTitleIcon();
			dispatchEvent(new Event( _open ? Event.OPEN : Event.CLOSE ));
			_openAnim.removeEventListener(EffectEvent.EFFECT_END, onEffectEnd);
		});
		_openAnim.play();
	}
	
}

Kind regards, Geert

Uma Shankar July 28, 2009 at 3:01 PM

Hi,

I am trying to down collapse the panel, just like in flex builder tasks panel.

I observed that since the height property is modified the panel looks like collapsing above. I want it to collapse below.

Hence I tried to create an animateproperty which modifies the y property.

The panel now moves down, but it doesnt fold or collapse, as earlier.

Can you please help me here.

The code is like this.

public function toggleOpen():void 
	{
		if (_creationComplete &amp;&amp; !_openAnimY.isPlaying &amp;&amp; !_openAnimHeight.isPlaying) {
			
			_openAnimY.fromValue = _openAnimY.target.y;
			_openAnimHeight.fromValue = _openAnimHeight.target.height;
			if (!_open) {
				_openAnimY.toValue = _openAnimY.target.y - openHeight + titleBar.height;
				
				_openAnimHeight.toValue = openHeight;
				_open = true;
				dispatchEvent(new Event(Event.OPEN));
			}else{
				_openAnimY.toValue = _openAnimY.target.y + openHeight - titleBar.height;
				
				_openAnimHeight.toValue = _openAnimHeight.target.closedHeight;
				
				_open = false;
				dispatchEvent(new Event(Event.CLOSE));
			}
			setTitleIcon();
			 _openAnimY.play();			_openAnimHeight.play();
			
		}
		
	}
Viral B September 30, 2009 at 4:17 AM

“The ColapsablePanel does not auto resize when the child size changes”.

In order to fix this:

  1. Comment out overridden invalidateSize() method.
  2. Fix updateOpen method. UpdateOpen is called when you assign value to open attribute. Let updateOpen method assign only closed height. No need to worry about open height.

private function updateOpen():void { if (!_open) height = closedHeight; // else height = openHeight; setTitleIcon(); }

Sekhar October 1, 2009 at 10:15 PM

Excellent work. You saved my day.

Thanks

kumar November 17, 2009 at 3:33 PM

Hi,

When iam using this iam getting Classes must not be nested.

Please help me.

Thanks, Praveen

kumar November 24, 2009 at 10:59 AM

Hi,

In provided .as file, the attaribute name titileBar is not defined.

Could you please explaine me what it is and where it needs to be defined.

Thanks, Kumar

Geert November 24, 2009 at 11:24 AM

@kumar The class CollapsiblePanel extends Panel and titleBar is a (protected) property of Panel.

You should certainly have a look at the Flex documentation @ http://livedocs.adobe.com/flex/3/langref/index.html

Few best Custom Components for Adobe Flex – Part 1? - WittySparks December 5, 2009 at 10:02 PM

[…] Normal Custom Collapsible Panel Normal Custom Collapsible Panel […]

Alan Cheung February 2, 2010 at 5:47 AM

Love this component. Thanks!

SuriForFlex June 29, 2010 at 12:37 AM

Hi Ali,

Great component!!

I was wondering if we could increase the width of panel when it opens. This width is greater than the width of CollapsiblePanel.

I would really appreciate your help

Thanks

Batkins July 7, 2010 at 1:41 AM

Advise the following changes for anyone looking to use this panel in order to get it to function and resize back to original proportions properly:

package com.containers {
   
    import flash.events.*;
    
    import mx.containers.Panel;
    import mx.core.ScrollPolicy;
    import mx.effects.AnimateProperty;
    import mx.events.*;
   
   
    /**
    * The icon designating a "closed" state
    */
    [Style(name="closedIcon", property="closedIcon", type="Object")]
   
    /**
    * The icon designating an "open" state
    */
    [Style(name="openIcon", property="openIcon", type="Object")]
   
    /**
    * This is a Panel that can be collapsed and expanded by clicking on the header.
    *
    * @author Ali Rantakari
    */
    public class CollapsiblePanel extends Panel {
       
        private var _creationComplete:Boolean = false;
        private var _open:Boolean = true;
        private var _openAnim:AnimateProperty;
        //used for resizing
        private var _defaultHeight:int;
        private var _usesPercentHeight:Boolean;
       
        /**
        * Constructor
        *
        */
        public function CollapsiblePanel(aOpen:Boolean = true):void
        {
            super();
            this.open = aOpen;
            this.addEventListener(FlexEvent.CREATION_COMPLETE, creationCompleteHandler);
            this.addEventListener(ResizeEvent.RESIZE, onResizeEvent);
        }
       
       
        // BEGIN: event handlers                ------------------------------------------------------------
       
        private function creationCompleteHandler(event:FlexEvent):void
        {
            this.horizontalScrollPolicy = ScrollPolicy.OFF;
            this.verticalScrollPolicy = ScrollPolicy.OFF;
                       
            _creationComplete = true;
           
            _openAnim = new AnimateProperty(this);
            _openAnim.duration = 300;
           
            titleBar.addEventListener(MouseEvent.CLICK, headerClickHandler);
        }
       
        private function headerClickHandler(event:MouseEvent):void { toggleOpen(); }
        
        private function onResizeEvent(event:ResizeEvent):void
        {
        	if (_open &amp;&amp; !_usesPercentHeight) this._defaultHeight = this.height;
        }
       
        private function callUpdateOpenOnCreationComplete(event:FlexEvent):void { updateOpen(); }
       
        // --end--: event handlers          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
       
       
       
        // BEGIN: private methods               ------------------------------------------------------------
       
        // sets the height of the component without animation, based
        // on the _open variable
        private function updateOpen():void
        {
            if (!_open) height = closedHeight;
            //else height = openHeight; - unnecessary, it will automatically be sized
            setTitleIcon();
        }
       
        // the height that the component should be when open
        private function get openHeight():Number {
            //return measuredHeight;
            return _defaultHeight;
        }
       
        // the height that the component should be when closed
        private function get closedHeight():Number {
            var hh:Number = getStyle("headerHeight");
            if (hh &lt;= 0 || isNaN(hh)) hh = titleBar.height;
            return hh;
        }
       
        // sets the correct title icon
        private function setTitleIcon():void
        {
            if (!_open) this.titleIcon = getStyle(&quot;closedIcon&quot;);
            else this.titleIcon = getStyle(&quot;openIcon&quot;);
        }
       
        // --end--: private methods         - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
       
       
       
       
       
       
       
       
        // BEGIN: public methods                ------------------------------------------------------------
        
        /**
		* Collapses / expands this block (with animation)
		*	this verison of the method contains a fix for the height not changing effectively
		*/
		public function toggleOpen():void
		{
		    if (_creationComplete &amp;&amp; !_openAnim.isPlaying) 
		    {
		        _openAnim.fromValue = _openAnim.target.height;
		        _openAnim.toValue = _open ? _openAnim.target.closedHeight : openHeight;
		        if (_usesPercentHeight) _openAnim.property = _open ? &quot;height&quot; : &quot;percentHeight&quot;;
		        else _openAnim.property = &quot;height&quot;;
		        _open = !_open;
		        _openAnim.addEventListener(EffectEvent.EFFECT_END, function onEffectEnd():void {
		            // Geert (8-Apr-2009)
		            // Only adapt the status &amp; title + send the event when the animation has done playing:
		            setTitleIcon();
		            dispatchEvent(new Event( _open ? Event.OPEN : Event.CLOSE ));
		            _openAnim.removeEventListener(EffectEvent.EFFECT_END, onEffectEnd);
		        });
		        _openAnim.play();
		    }
		}
       
       
        /**
        * Whether the block is in a expanded (open) state or not
        */
        public function get open():Boolean {
            return _open;
        }
        /**
        * @private
        */
        public function set open(aValue:Boolean):void {
            _open = aValue;
            if (_creationComplete) updateOpen();
            else this.addEventListener(FlexEvent.CREATION_COMPLETE, callUpdateOpenOnCreationComplete, false, 0, true);
        }
        
		override public function set percentHeight(value:Number):void
		{
			if (_open)
			{
				super.percentHeight = value;
				if (!isNaN(value))
				{
					this._defaultHeight = value;
					this._usesPercentHeight = true;
				}
				else
					this._usesPercentHeight = false;
			}
		}
       
        // --end--: public methods          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
       
       
    }
}
Jarno September 9, 2010 at 8:57 AM

Hi! Thank you for this component. I have a canvas inside this panel because of absolute layout for my components. I have dynamic size text component inside panel. This Collapsible panel does not size itself compared text component. I only see first line of text component.

In other word this panel does not know how to set it’s height compared childElements.

Andreas March 7, 2012 at 7:42 PM

Hi

We’ve been using this component successfully for a couple of years now. However we’re in the process of upgrading Flex 3 to Flex 4.5. While it is not necessary to move to a spark collapsible component…it seems that anything that’s contained within the CollapsiblePanel does not get added into the display list….the creationComplete handler does not get invoked….

any ideas anyone?

How to add Min/Max buttons to <mx:Panel> » free icons download June 14, 2012 at 12:18 PM

[…] corner. Could some one let me know what is the best approach? ||| I think you need to use AIR. ||| ://hasseg.org/blog/post/113/collapsible-panel-component-for-flex/ http://blogs.adobe.com/flexdoc/2007/03/creating_resizable_and_draggab. html […]

Priya August 5, 2012 at 3:02 AM

Hi,

Thanks so much for this component. I have a small doubt. I would like to know whether i could some buttons to the title bar. Because i dint find any button option in the functions available. If i need to add, what should i do?

Thanks again.

Greg August 6, 2012 at 10:40 PM

This is really great component, but I’m having a lot of problems changing its header’s color. It doesn’t work with spark. The property called headerColors, which is an array of two colors, is ignored by spark. I know I can add argument to my project properties to tell the compiler to include ‘halo’ as well, but that messes up a lot of things in my projec, so I can’t use ‘halo’. I was wondering then, if there’s any other way to change colors of the header. It is always gray. Thanks a lot

Categories