Template Components
Even though all the components within the Flex framework are descendants of UIComponent, and they perform a variety of functions, there is a super set of components that exhibit a particular style of creation and data management commonly referred to as the template components. Don't confuse them with the common definition of a template by thinking that these components are like blueprints and instances are made from them. No, these components define and manage what types of child components are declared by providing the flexibility for those component types to be determined at implementation time, not as part of their actual code base.
This may sound too good to be true, but it is another example of the lateral-ness of component development. Yes, the ability to drop additional components onto a container component and have them all add themselves to the correct display lists and such is because of templating. Template components need not just be big containers; you can define aspects of your component to allow only certain types of components to be included. However, before you start to glaze over with information overload, let's go over how template components actually implement this templating functionality.
Deferring Instantiation
Template components employ a process known as deferred instantiation. Deferred instantiation enables you to define an aspect of your component as a marker that then creates its child assets from the class descriptors when required. That way, you can provide a general data type for your component that can then be replaced with a concrete version of it as required. The beauty of this is that you provide a real level of flexibility to your components, allowing a developer the ability to use any component that conforms to the general type that was initially defined. Deferred instantiation usually comes in two variants: by class or via the interface IDeferredInstance. The process by which you define your template areas is similar, as the following Halo example shows. I highlighted the key areas so that you can easily recognize them.
package com.developingcomponents.components { import mx.core.UIComponent; import spark.components.Group; import spark.layouts.HorizontalLayout; import spark.layouts.supportClasses.LayoutBase; public class UIComponentTemplateExample extends UIComponent { private static const DEFAULT_WIDTH :uint = 200; private static const DEFAULT_HEIGHT :uint = 30; private var _controlBar :Group; private var _layout :LayoutBase; [Bindable] [ArrayElementType("spark.components.Button")] public var controlBar :Array; public function UIComponentTemplateExample() { super(); _layout = new HorizontalLayout(); } override protected function createChildren():void { super.createChildren(); _controlBar = new Group(); _controlBar.layout = _layout; addChild(_controlBar); } override protected function commitProperties():void { super.commitProperties(); createDeferredItems(); } override protected function measure():void { super.measure() measuredWidth = measuredMinWidth = DEFAULT_WIDTH; measuredHeight = measuredMinHeight = DEFAULT_HEIGHT; _controlBar.setActualSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); } override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { super.updateDisplayList(unscaledWidth, unscaledHeight); width = unscaledWidth; height = unscaledHeight; _controlBar.setActualSize(width, height); } private function createDeferredItems():void { for (var i :int = 0; i < controlBar.length; i++) { _controlBar.addElement(controlBar[i]); } } public function get layout():LayoutBase { return _layout; } public function set layout(value:LayoutBase):void { _layout = value; _controlBar.layout = value; } } }
As you can see, this component uses an array that has some metadata applied to it. (If you have not used metadata before, don't worry; you can find out more about it in Chapter 7, "Working with Metadata.") This particular metadata sets the ArrayElementType to spark.components.Button. Although we cannot type arrays, we can make sure that, if external data is provided, it is of the type we require via the use of the ArrayElementType metadata block; in this case, it is typed to Spark Button components.
After we have our array of Button components, we need to process them so that they can be added to the relevant display list. Most of the time, you want to do this either via commitProperties() or creationComplete(), depending on whether you are creating an ActionScript or MXML-based component. In the case of the previous code example, we are adding it to a control bar called _controlBar (original name!) via the commitProperties() method. One thing to remember is that, although this example is fairly explicit in the type of child asset that can be supplied to the control bar, you can be more general. For example, there is nothing stopping me from changing the ArrayElementType to mx.core.UIComponent if I wanted. Although, from a functional point of view, you wouldn't want to give a developer the ability to place a Panel component inside your control bar—or would you?
Implementing interfaces as a form of deferred instantiation is almost identical. Again, there is an ArrayElementType block over the array used for our child elements. However, as you have probably noticed, instead of a concrete class, we now instead have an interface declared. If you look further down, you see that it uses the same loop to add our buttons as in the previous example. Instead of having the ArrayElementType defined as spark.components.Button, it is defined as mx.core.IDeferredInstance, and the loop to add them as children of the control bar is initially cast as Button, which subsequently calls the getInstance() method to create the child asset. For brevity, I included only the main changes in the previous example when implemented via IDeferredInstance.
package com.developingcomponents.components { ... import spark.components.Button; [Bindable] [ArrayElementType("mx.core.IDeferredInstance")] public var controlBar :Array; ... private function createDeferredItems():void { for (var i :int = 0; i < controlBar.length; i++) { _controlBar.addElement(Button( IDeferredInstance( controlBar[i]).getInstance())); } } ... }
The same result can be achieved from either option. IDeferredInstance provides far more flexibility because you can instantiate any class that implements the IDeferredInstance interface; although to achieve this, you need to either make sure you cast your class to the lowest common denominator or, in the case of the example code, explicitly cast it to the type of element you require (a Spark Button, in this case). The only caveat to using IDeferredInstance is that the constructor for the class being instantiated (such as the Button components that are passed in) cannot have any parameters passed as part of the constructor signature, which, as you have already seen, is a requirement for constructors in components. So, no worries there.
Before we move on, you may be wondering why we use arrays for our deferred elements instead of using a vector? If you were assigning concrete class instances to your template component, then yes, you can remove the metadata and the array and replace it with a vector. Unfortunately, if you try this for the IDeferredInstance version, you get an error when assigning the Button elements via MXML because, as far as the compiler is concerned, these are Spark Button components and, as such, they don't provide a hook to the IDeferredInstance interface.