Thursday, February 16, 2012

Custom WorkFlow Navigator Component

Recently I tried to create one simple and easy-to-use workflow navigator component. Suppose we want to fill one form which has five steps then this component can be used as workflow at top to show these steps. The end user would be able to click on any of the steps can move back and forth. This navigator takes String and Workflow items. If we give strings it will convert them to work-flow items before insertion. This code can still be refined and will be done periodically basis.



The main component class is WorkFlowNavigator which handles the complete logical aspects. This component extends SkinnableComponent and starts from the scratch. The class WorkFlowItem is responsible for displaying the items in the navigator. The navigator also includes next/previous arrow buttons which can be used to navigate. This component also provides option of clicking any of the workflow item (in case user wants to move back to that step), in that case navigator will be updated and all work-flow items following the selected item will be removed. Moreover an event will be dispatched to notify the user.



package mittal.components

{

    import flash.events.Event;

    import flash.events.MouseEvent;

   

    import mittal.events.WorkFlowEvent;

    import mittal.skins.WorkFlowNavigatorSkin;

    import mittal.valueObjects.WorkFlowItem;

   

    import mx.collections.ArrayCollection;

    import mx.collections.IList;

    import mx.controls.Alert;

    import mx.core.IFactory;

   

    import spark.components.Button;

    import spark.components.HGroup;

    import spark.components.supportClasses.SkinnableComponent;

   

    //--------------------------------------

    //  Events

    //--------------------------------------

    /**

     *  Dispatched when the data provider changes.

     *

     *  @eventType mittal.events.WorkFlowEvent.WORKFLOW_DATA_CHANGED

     * 

     *  @langversion 3.0

     *  @playerversion Flash 10

     *  @playerversion AIR 1.5

     *  @productversion Flex 4

     */

    [Event(name="workFlowDataProviderChanged", type="mittal.events.WorkFlowEvent")]

    /**

     *  Dispatched when the selected index changes.

     *

     *  @eventType mittal.events.WorkFlowEvent.SELECTED_INDEX_CHANGED

     * 

     *  @langversion 3.0

     *  @playerversion Flash 10

     *  @playerversion AIR 1.5

     *  @productversion Flex 4

     */

    [Event(name="selectedIndexChanged", type="mittal.events.WorkFlowEvent")]

    /**

     *  Dispatched when the next button is clicked.

     *

     *  @eventType mittal.events.WorkFlowEvent.WORKFLOW_NEXT_CLICK

     * 

     *  @langversion 3.0

     *  @playerversion Flash 10

     *  @playerversion AIR 1.5

     *  @productversion Flex 4

     */

    [Event(name="nextClicked", type="mittal.events.WorkFlowEvent")]

    /**

     *  Dispatched when the previous button is clicked.

     *

     *  @eventType mittal.events.WorkFlowEvent.WORKFLOW_PREVIOUS_CLICK

     * 

     *  @langversion 3.0

     *  @playerversion Flash 10

     *  @playerversion AIR 1.5

     *  @productversion Flex 4

     */

    [Event(name="prevClicked", type="mittal.events.WorkFlowEvent")]

    /**

     *  Dispatched when the work flow item is clicked by user.

     *

     *  @eventType mittal.events.WorkFlowEvent.WORKFLOW_ITEM_SELECTED_BY_USER

     * 

     *  @langversion 3.0

     *  @playerversion Flash 10

     *  @playerversion AIR 1.5

     *  @productversion Flex 4

     */

    [Event(name="workFlowItemClicked", type="mittal.events.WorkFlowEvent")]



    /**

     *  The WorkFlowNavigator component displays a list of workflow items.

     *  Its functionality is similar to the standard work flow component.

     *  Based on the items in data provider it creates a number of work-flow items

     *  and the first and last items will have different representations.

     *  This component is based upon SkinnableComponent and overrides a number of methods.

     *  This class also provides implementation of getCurrentSkinState() method to retrun proper value to its skin class.

     * 

     *  There will not be any scrollbar so proper width and height must be selected.

     *

     *  @langversion 3.0

     *  @playerversion Flash 10

     *  @playerversion AIR 1.5

     *  @productversion Flex 4

     */

    public class WorkFlowNavigator extends SkinnableComponent

    {

        //--------------------------------------------------------------------------

        //

        //  Class constants

        //

        //--------------------------------------------------------------------------

       

        /**

         *  Static constant representing the value "no selection".

         *

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        public static const NO_SELECTION:int = -1;

        private static const DEFAULT_HEIGHT :Number = 200;

        private static const DEFAULT_WIDTH :Number = 200;

        public static const FIRST_ITEM:int = 0;

        private var LAST_ITEM:int;

       

        [SkinPart(required="true", type="mittal.components.WorkFlowButton")]

        /** * A dynamic skin part that defines a WorkFlow Item */

        public var workFlowButton:IFactory;

       

        [SkinPart(required="false")]

        public var nextButton:Button;

        [SkinPart(required="false")]

        public var previousButton:Button;

        [SkinPart(required="true")]

        public var workFlowBar:HGroup;

       

        //--------------------------------------------------------------------------

        //

        //  Constructor

        //

        //--------------------------------------------------------------------------

       

        public function WorkFlowNavigator()

        {

            super();

            addEventhandlers();

            setStyle("skinClass",mittal.skins.WorkFlowNavigatorSkin);

        }

       

        //--------------------------------------------------------------------------

        //

        //  Properties

        //

        //--------------------------------------------------------------------------

       

        /**

         *  @private

         *  Flag that is set when the selectedIndex has been adjusted due to

         *  user interaction or next/prev button click.

         *  This flag is cleared in commitProperties().

         */

        private var selectedIndexChanged:Boolean =  false;

        /**

         *  The 0-based index of the selected item, or -1 if no item is selected.

         *  Setting the <code>selectedIndex</code> property deselects the currently selected

         *  item and selects the data item at the specified index.

         *

         *  <p>The value is always between -1 and (<code>dataProvider.length</code> - 1).

         *

         *  @default -1

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        private var _selectedIndex:int = NO_SELECTION;

       

        [Bindable("selectedIndexChanged")]

        public function get selectedIndex():int

        {

            return _selectedIndex;

        }

        /**

         *  @private

         */

        public function set selectedIndex(value:int):void

        {

            if(value == _selectedIndex)

                return;

           

            _selectedIndex = value;

            selectedIndexChanged = true;

            invalidateProperties();

        }

        //----------------------------------

        //  hovered

        //----------------------------------

       

        /**

         *  @private

         *  Storage for the hovered property

         */

        private var _hovered:Boolean = false;   

       

        /**

         *  Indicates whether the mouse pointer is over the button.

         *  Used to determine the skin state.

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        protected function get hovered():Boolean

        {

            return _hovered;

        }

       

        /**

         *  @private

         */

        protected function set hovered(value:Boolean):void

        {

            if (value == _hovered)

                return;

           

            _hovered = value;

            invalidateSkinState();

        }

        /**

         *  @private

         */

        private var dataProviderChanged:Boolean;

        /**

         *  @private

         */

        private var doingWholesaleChanges:Boolean = false;

       

        //----------------------------------

        //  dataProvider

        //----------------------------------

       

        private var _dataProvider:*;

       

        [Bindable("workFlowDataProviderChanged")]

        public function get dataProvider():*

        {

            return _dataProvider;

        }

        public function set dataProvider(value:*):void

        {

            if(_dataProvider == value)

                return;

           

            _dataProvider = value;

            dataProviderChanged = true;

            invalidateProperties();

        }

       

        //--------------------------------------------------------------------------

        //

        //  Methods

        //

        //--------------------------------------------------------------------------

       

        private function nextButtonHandler(event:MouseEvent):void {

            if(selectedIndex < (workFlowBar.numElements -1))

                selectedIndex++;

            dispatchEvent(new WorkFlowEvent(WorkFlowEvent.WORKFLOW_NEXT_CLICK));

        }

        private function previousButtonHandler(event:MouseEvent):void {

            if(selectedIndex > 0)

                selectedIndex--;

            dispatchEvent(new WorkFlowEvent(WorkFlowEvent.WORKFLOW_PREVIOUS_CLICK));

        }

        /**

         *  This method  will deselect all the button except the one clicked by the user.

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        private function updateSelectedWorkFlowItem(btnIndex:uint):void

        {

            //Enable the selected one and disable the others.

            var totalWorkFlowItems:int = workFlowBar.numElements;

            var workFlowItem:WorkFlowButton;

            for (var i:int = 0; i<totalWorkFlowItems; i++)

            {

                workFlowItem = workFlowBar.getElementAt(i) as WorkFlowButton;

                workFlowItem.isDown = true;

                workFlowItem.invalidateSkinState();

            }

            (workFlowBar.getElementAt(btnIndex) as WorkFlowButton).isDown = false;

        }

        /**

         *  When user clicks on workflowItem the selectedIndex is updated and then same event is

         *  forwarded to the user.

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        private function workflowItemManualSelectionHandler(event:WorkFlowEvent):void

        {

            selectedIndex = (event.buttonIndex != -1)?(event.buttonIndex):0;

            dispatchEvent(event.clone());

        }

        /**

         *  This method mainly converts the item to workflow item and then calls up addProperWorkFlowItem to add this item.

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        protected function createAndAddProperWorkFlowItems(item:*, index:int):void

        {

            var workFlowItem:WorkFlowItem;

           

            if(item is WorkFlowItem)

            {

                addProperWorkFlowItem(item, index);

            }

            else if(item is String)

            {

                workFlowItem = new WorkFlowItem();

                workFlowItem.label = item;

                workFlowItem.id = "11";

                addProperWorkFlowItem(workFlowItem, index);

            }

        }

        /**

         *  This method mainly does the actual insertion of work flow item.

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        protected function addProperWorkFlowItem(item:WorkFlowItem, index:int):void

        {

            var workFlowItem :WorkFlowButton = createDynamicPartInstance("workFlowButton")as WorkFlowButton;

            workFlowItem.label = item.label;

            workFlowItem.id = item.id;

            workFlowItem.btnIndex = index;

            workFlowItem.toolTip = item.label;

           

            if(index == FIRST_ITEM)

            {

                workFlowItem.setStyle("skinClass",mittal.skins.FirstButtonSkin);

            }

            else if (index == LAST_ITEM)

            {

                workFlowItem.setStyle("skinClass",mittal.skins.LastButtonSkin);

            }

            else

            {

                workFlowItem.setStyle("skinClass",mittal.skins.MiddleButtonSkin);

            }

            workFlowBar.addElement(workFlowItem);

        }

        /**

         *  This method is called upon when dataprovider changes. This function will remove all previous items and

         *  insert new items.

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        protected function refreshWorkFlowItems(itemsCollection:ArrayCollection):void

        {

            var itemsCount:int = itemsCollection.length;

            if(itemsCount > 2)

            {

                for(var index:int = 0; index<itemsCount; index++)

                {

                    createAndAddProperWorkFlowItems(itemsCollection.getItemAt(index),index);

                }

            }

            else

                Alert.show("Please provide atleast 3 items.","Error in Usage");

        }

        /**

         *  This method should be used to perform all clean up operations.

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        protected function destroyWorkFlow():void

        {

            removeEventHandlers();

        }

        /**

         *  This method handles the mouse events, calls the <code>clickHandler</code> method

         *  where appropriate and updates the <code>hovered</code> and

         *  <code>mouseCaptured</code> properties.

         *

         *  <p>This method gets called to handle <code>MouseEvent.ROLL_OVER</code>,

         *  <code>MouseEvent.ROLL_OUT</code>, <code>MouseEvent.MOUSE_DOWN</code>,

         *  <code>MouseEvent.MOUSE_UP</code>, and <code>MouseEvent.CLICK</code> events.</p>

         *

         *  @param event The Event object associated with the event.

         * 

         *  @langversion 3.0

         *  @playerversion Flash 10

         *  @playerversion AIR 1.5

         *  @productversion Flex 4

         */

        protected function mouseEventHandler(event:Event):void

        {

            var mouseEvent:MouseEvent = event as MouseEvent;

            switch (event.type)

            {

                case MouseEvent.ROLL_OVER:

                {

                    hovered = true;

                    break;

                }

                   

                case MouseEvent.ROLL_OUT:

                {

                    hovered = false;

                    break;

                }

            }

        }

        /**

         *  @private

         */

        private function addEventhandlers():void

        {

            addEventListener(MouseEvent.ROLL_OVER, mouseEventHandler);

            addEventListener(MouseEvent.ROLL_OUT, mouseEventHandler);

        }

        /**

         *  @private

         */

        private function removeEventHandlers():void

        {

            removeEventListener(MouseEvent.ROLL_OVER, mouseEventHandler);

            removeEventListener(MouseEvent.ROLL_OUT, mouseEventHandler);

        }



        //--------------------------------------------------------------------------

        //

        //  Overridden Methods

        //

        //--------------------------------------------------------------------------

        /**

         *  @private

         */

        override protected function measure():void

        {

            super.measure();

            measuredMinHeight = measuredHeight = DEFAULT_HEIGHT;

            measuredMinWidth = measuredWidth = DEFAULT_WIDTH;

        }

        /**

         *  @private

         */

        override protected function commitProperties():void

        {

            if(selectedIndexChanged)

            {

                selectedIndexChanged = false;

                updateSelectedWorkFlowItem(selectedIndex);

                dispatchEvent(new WorkFlowEvent(WorkFlowEvent.SELECTED_INDEX_CHANGED));

            }

            if(dataProviderChanged)

            {

                dataProviderChanged = false;

                LAST_ITEM = IList(dataProvider).length -1;

                refreshWorkFlowItems(dataProvider);

                dispatchEvent(new WorkFlowEvent(WorkFlowEvent.WORKFLOW_DATA_CHANGED));

            }

            super.commitProperties();

        }

        /**

         *  @private

         */

        override protected function partAdded(partName:String, instance:Object):void {

            super.partAdded(partName, instance);

           

            if (instance == nextButton) {

                (instance as Button).addEventListener(MouseEvent.CLICK, nextButtonHandler);

            }

            if (instance == previousButton) {

                (instance as Button).addEventListener(MouseEvent.CLICK, previousButtonHandler);

            }

            if(partName == 'workFlowButton'){

                Button(instance).addEventListener(WorkFlowEvent.WORKFLOW_ITEM_SELECTED_BY_USER,workflowItemManualSelectionHandler);

            }

        }

        /**

         *  @private

         */

        override protected function partRemoved(partName:String, instance:Object):void {

            if (instance == nextButton) {

                (instance as Button).removeEventListener(MouseEvent.CLICK, nextButtonHandler);

            }

            if (instance == previousButton) {

                (instance as Button).removeEventListener(MouseEvent.CLICK, previousButtonHandler);

            }

            if(partName == 'workFlowButton'){

                Button(instance).removeEventListener(WorkFlowEvent.WORKFLOW_ITEM_SELECTED_BY_USER,workflowItemManualSelectionHandler);

            }

            super.partRemoved(partName, instance);

        }

        /**

         *  @private

         */

        override protected function getCurrentSkinState():String

        {

            if (!enabled)

                return "disabled";

           

            if (hovered)

                return "over";

           

            return "up";

        }

       

    }

}



Every workflow item is represented by  WorkFlowButton as:





package mittal.components

{

    import flash.events.Event;

    import flash.events.MouseEvent;

   

    import mittal.events.WorkFlowEvent;

    import mittal.skins.FirstButtonSkin;

   

    import spark.components.Button;

   

    /**

     *  Dispatched when user clicks on any of the WorkFlow Item (Button). This will require the selectedIndex property of the navigator to be updated.

     *

     *  @eventType mittal.events.WorkFlowEvent.WORKFLOW_ITEM_SELECTED_BY_USER

     * 

     *  @langversion 3.0

     *  @playerversion Flash 10

     *  @playerversion AIR 1.5

     *  @productversion Flex 4

     */

    [Event(name="workFlowItemClicked", type="mittal.events.WorkFlowEvent")]

   

    public class WorkFlowButton extends Button

    {

        private var _btnIndex:uint;

        public var isDown:Boolean;

       

        [Bindable(event="workFlowItemIndexChanged")]

        public function get btnIndex():uint

        {

            return _btnIndex;

        }

        public function set btnIndex(value:uint):void

        {

            if(value == btnIndex)

                return;

            _btnIndex = value;

            dispatchEvent(new Event("workFlowItemIndexChanged"));

        }

        public function WorkFlowButton()

        {

            setStyle("skinClass",FirstButtonSkin);

            addEventListener(MouseEvent.CLICK, mouseClickHandler);

            super();

        }

        protected function mouseClickHandler(event:MouseEvent):void

        {

            //It need not to do any skin change as this would be done automatically once selectedIndex of navighator is set.

            event.preventDefault();

            event.stopImmediatePropagation();

            dispatchEvent(new WorkFlowEvent(WorkFlowEvent.WORKFLOW_ITEM_SELECTED_BY_USER,btnIndex));

        }

        override protected function commitProperties():void

        {

            super.commitProperties();

        }

        override protected function getCurrentSkinState():String

        {

            return (isDown ? 'buttonDown' : super.getCurrentSkinState());

        }

    }

}




I have used six skins: One for the First Button, One for Middle buttons, One for the Last Button, One for Next Button, One for Previous Button and One for the WorkFlowNavigator component. The skin for the First Button will be like below. The important thing to note is the HostComponent and Path tag used to create the shape.


<s:SparkSkin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark"

             xmlns:fb="http://ns.adobe.com/flashbuilder/2009" minWidth="21" minHeight="21" alpha.disabled="0.5">

   

    <!-- host component -->

    <fx:Metadata>

        <![CDATA[

        /**

         * @copy spark.skins.spark.ApplicationSkin#hostComponent

         */

        [HostComponent("mittal.components.WorkFlowButton")]

        ]]>

    </fx:Metadata>

   

    <!-- states -->

    <s:states>

        <s:State name="up" />

        <s:State name="over" />

        <s:State name="down" />

        <s:State name="disabled" />

        <s:State name="buttonDown" />

    </s:states>

   

    <s:Graphic>

        <s:Path data="m 0 0

                h 100

                l 20 20

                l -20 20

                h -100

                v -40">

            <s:fill>

                <s:LinearGradient rotation="90">

                    <!-- Note the use of different colors based on state -->

                    <s:GradientEntry color="0xfd1901" color.buttonDown="0xAF220D" />

                    <s:GradientEntry color="0xa70d12" color.buttonDown="0xAF220D" />

                </s:LinearGradient>

            </s:fill>

            <s:stroke>

                <s:SolidColorStroke color="0xAF220D" weight="1" alpha=".8" alpha.disabled="0"  alpha.up="0"/>

            </s:stroke>

        </s:Path>

    </s:Graphic>

   

    <!-- layer 8: text -->

    <!--- @copy spark.components.supportClasses.ButtonBase#labelDisplay -->

    <s:Label id="labelDisplay"

             textAlign="left"

             verticalAlign="middle"

             maxDisplayedLines="1"

             color="white"

             horizontalCenter="0" verticalCenter="1"

             left="0" right="10" top="8" bottom="2">

    </s:Label>

   

</s:SparkSkin>



And finally this component will be used as:


<fx:Script>

        <![CDATA[

            import mittal.events.WorkFlowEvent;

           

            import mx.collections.ArrayCollection;

            import mx.controls.Alert;

            private function creationComplete():void

            {

                wf.dataProvider = new ArrayCollection(["one", "two", "three", "four", "five","six","seven","eight","nine","ten"]);

            }

            protected function bd_workFlowItemClickedHandler(event:WorkFlowEvent):void

            {

                Alert.show("Item clicked"+ event.buttonIndex);

            }



        ]]>

    </fx:Script>

    <s:layout>

        <s:VerticalLayout/>

    </s:layout>

    <components:WorkFlowNavigator id="wf" width="100%" height="60"

                                  workFlowItemClicked="bd_workFlowItemClickedHandler(event)"

                                  />



I have not been able to find a proper way of highlighting actionscript code and we also dont have an option to upload source code, so I have provided the complete source folder here.

This workflow resembles to some extent the workflow component we get in google blogger itself when we explore setting etc. So Did you find this post helpful? What would you like to be improved/added? Please spent some time to provide your feedback that can help improve it.



12 comments:

Anonymous said...

This is good but how can i write my own component?

Anonymous said...

Hello!! Is a very good component, but i can't download the source from adobe. You can send me to my mail?

THX

Akhil Mittal said...

Hi,

You can give your email id here. I will post you the complete project that you can import in Flash BUilder.

Thanks,
Akhil Mittal

Leandro S. Ghiglieri said...

Akhil, thank you very much for your fast response.
My name is Leandro, and my e-mail is racingleo@gmail.com.
Thanks in advance!

Akhil Mittal said...

Hi Leandro,

Hope you have received the zip and it s working fine.

Thanks,
Akhil

charlie_4_luv said...
This comment has been removed by the author.
Vivek Kushwaha said...
This comment has been removed by the author.
Vivek Kushwaha said...

Hello Akhil,

Your blog is very helpful. Only because of you blog I got answers of many difficult questions.

I want to learn create custom components in ActionScript for SDK 3.0 as well as 4. I am aware about whole life cycle of components. But still feeling some difficulties when I go n start writing. So can you please share some examples or some links so that things become more clear to me.

my mail id is vivek.kushwaha46@live.in

-Vivek

爛芋頭 said...

Hi, I google flex breadcrumbs components and find your blog, it's great. But i can not find the source. the adobe forum link not valid. Could you send to my mail, thanks. my mail is yestaro@gmail.com

Akhil Mittal said...

I will check if I can locate the code and will send you.

sumit sharma said...

HI - If any has source code, please send it to me also at sumitsearch2006@gmail.com

Akhil Mittal said...

Sumit, I wrote it long back, need to check if I still has the source code.