Flex - Template System Using Regular Expressions & Simple OOP

I think it goes without saying that we all want to be regular expression hipsters like Ben Nadel ... maybe it's just me? Anyway, there have been many occasions when a regular expression would have been the best tool for the job, but I wimped out and went with more cumbersome string manipulations with code. For a recent messaging system using templates, I started down that road but, luckily found a compelling reason to force myself to use the ActionScript RegEx class.

So the quick skinny is that I have a template with placeholders that I need to substitute completely before moving on. For the purposes of discussion, the reasons for the system will be unexplored and I'll try to leave everything simple enough to allow easy re-purposing.

The placeholder for the substitution needs to be fairly unique so that we don't collide with something that ought to be displayed. In this case, we'll denote all of the substitution values with the following form:

${some_value}

When I first started going this route, I was able to skip regular expressions by just looking for explicitly named values and doing global substitutions. And to make matters easier, I was performing the replacement on the sever side. Actually that is probably worthy of a separate discussion as I hit the template from two angles: explicit substitution and also by iterating over an associative array (Struct) of replacement values. Anyway, looking for ${FIRST_NAME} could be performed on the server thus:

<cfscript>
// other code

returnMessage = arguments.template;
if (FindNoCase("${FIRST_NAME}",template) >
0) {
template = ReplaceNoCase(template,'${FIRST_NAME}',getFirstName(),"all");
}

// other code
</cfscript>

That's just a global replace where the replacement value comes from a function named getFirstName().

This system could be, and I've certainly done it, adapted to handle simple replacement as well as complex scenarios where the replacement value is a function of additional business logic, database queries and the like.

More interesting for the interface side, however, is creating a template that solicits input from the user. In this case, we need the placeholder to convey additional information. At a minimum, we need to know that the system should ask for more information, what kind of information it will be, and then what to tell the user so that s/he enters in the desired info. For this, we'll use the following form:

${INPUT|TYPE|LABEL}

So, if we wanted to ask the user her birthday, we could use something like:

${INPUT|DATE|What is your birthday?}

For my needs, I identified the following datatypes:

  • TEXT
  • TEXTAREA
  • NUMBER
  • MONEY
  • DATE

TEXT yields a TextInput, TEXTAREA a TextArea, NUMBER a NumericStepper, MONEY a NumericStepper that is formatted with a CurrencyFormatter and DATE a DateField.

So that's the background, let's look at making this all work.

To keep the complexity down, we'll just a have a simple interface with a TextArea and a Button. When we click the button, the text in the TextArea will be parsed, input solicited and then the given values will replace the placeholders.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Button label="Test" left="10" top="10"/>
<mx:TextArea id="taTest" top="40" left="10" height="400" width="450">
<mx:text><![CDATA[

${INPUT|TEXT|Programming language?} is ${INPUT|TEXT|Adjective?}.
I've been programming in it since ${INPUT|DATE|Date?}.    
        
]]>
</mx:text>        
</mx:TextArea>    
</mx:Application>
I've thrown in a MadLibs style template.

Regular Expressions To The Rescue

The pattern I used to find all of the inputs was:

/\${INPUT\|\w+\|[^\}.]+\}/g

Essentially, we are looking for the string ${INPUT| followed by a word (alphanumeric, no spaces), followed by a pipe, "|", followed by one or more characters that are not a closing curly brace, "}", that is followed by a closing curly brace, "}". And the "/g" gets all of them for us. If we don't exclude the closing curly brace, multiple placeholders would appear only as one very long one.

So, to use this, we need to look at the TextArea and match all of the inputs. Once we do that, we need to parse the internal string and grab the data type and the label. There is more than one way to skin this cat, however, since we are already using regular expressions, why not use another one?

The first pattern will find something of the form ${INPUT|TYPE|LABEL} and what we want to do is to grab just the INPUT|TYPE|LABEL. Actually, we only want the TYPE|LABEL, but it is easier to grab it all and then just ignore the INPUT later on. The one that worked for me was:

/INPUT\|\w+\|[^\}.]+/

You'll notice that it is remarkably similar to the first one. We are really just stripping off the ${} so that shouldn't be too surprising. What we get will be a string with the pipes, "|", but that is not a problem. We can use the string method "split" to break this into an Array using the pipes as delimiters.

And lastly, before dropping some actual script on you, I want everything to packaged in a transportable object that I can use later to build a user interface. An array of these input arrays will make life easier.

So, since I gave the button a label of test, let's go ahead and create a test function that will do all of the above:

private function test():void {
var _pattern:RegExp = new RegExp(/\${INPUT\|\w+\|[^\}.]+\}/g);
var _matchPattern:RegExp = /INPUT\|\w+\|[^\}.]+/;
var _matches:Array = taTest.text.match(_pattern);
            
var _uiPieces:Array = [];
for (var m:int=0;m<_matches.length;m++) {                
_uiPieces.push( _matchPattern.exec(_matches[m])[0].toString().split("|") );
}
}

The first two lines are the regular expressions discussed above. Just for fun, I instantiated them differently to demonstrate that you can explicitly create the object by creating a new instance of the RegExp class or you can just use a literal; whatever floats your boat.

Next, we look for all of the input placeholders and store it into an Array. In this case, we are using a string method, match, to do the work.

As I mentioned before, I want a handy, easy-to-transport Array and that is the _uiPieces. And we will populate that guy with the Arrays formed by splitting the innards of the placeholder. Note here that it is the exec method of the regular expression that is doing the work.

Of course, this is nice, but it doesn't actually do anything. So we need to do something with the _uiPieces array. The real remaining work is all about user interface and I've choosen to do this as a modal TitleWindow. Our TitleWindow will need a public property, a setter, to receive the _uiPieces array, and it will need to throw a couple of events: a complete event and a close event. The close event might not actually be needed, but, at least during testing, it is nice to have a working close button so you don't get stuck. The next step is to call a function that will launch this component, set the property, add the listeners and generally move us along. We will also go over the event listeners so that we can wrap up this section and move to the fun ui stuff.

private function test():void {
var _pattern:RegExp = new RegExp(/\${INPUT\|\w+\|[^\}.]+\}/g);
var _matchPattern:RegExp = /INPUT\|\w+\|[^\}.]+/;
var _matches:Array = taTest.text.match(_pattern);
            
var _uiPieces:Array = [];
for (var m:int=0;m<_matches.length;m++) {                
_uiPieces.push( _matchPattern.exec(_matches[m])[0].toString().split("|") );
}        
showInput(_uiPieces); // ADDED
}

import mx.managers.PopUpManager;

private function showInput(inputs:Array):void {
var _inputWin:InputDialog = PopUpManager.createPopUp(this,InputDialog,true) as InputDialog;
_inputWin.addEventListener(InputDialog.EVENT_CLOSE,closeInputWindow);
_inputWin.addEventListener(InputDialog.EVENT_COMPLETE,populateTemplate);
_inputWin.inputs = inputs;
PopUpManager.centerPopUp(_inputWin);
}

private function closeInputWindow(e:Event):void {
PopUpManager.removePopUp(e.target as InputDialog);
}

private function populateTemplate(e:Event):void {
var _replacementValues:Array = (e.target as InputDialog).formValues;

for (var r:int=0;r<_replacementValues.length;r++) {
taTest.text = taTest.text.replace(_replacementValues[r][2],_replacementValues[r][1]);
}            
closeInputWindow(e);
}

We probably only really need to go over the populateTemplate function, but, to be complete, let's go over all of the changes. At the end of test(), we make the call to showInput(). showInput uses the PopupManager, so we need to import that call. We can use a local variable to handle the modal popup and to support the event listeners, we are going to create some static constants in our custom class, InputDialog. We'll get to creating that in just a moment. We will be creating the setter in InputDialog as well and I am planning on naming it inputs. Also, I like to center my popups; I think it looks better.

The closeInputWindow() is pretty straightforward. If the application was going to support lots of different popups, I might cast the e.target more generically. In this case, it doesn't really matter. It is by using the Event.target that we can skip creating a more permanent reference to the InputDialog.

The populateTemplate() is going to reference a getter that we will create called formValues. Like _uiPieces, it will be an array of arrays. To facilitate the final substitution, one of the array items will be a reference to the original placeholder and another will be the entered value. We iterate over the outer array, perform the substitution and then close the modal popup by passing the event parameter to the closeInputWindow; code reuse is nice.

Dynamically Generated UI

The InputDialog needs to parse the inputs and create a user facing form on the fly. The only real trick to this guy was normalizing the various inputs. That is, to reference a TextInput or TextArea, one uses the text property, for the Date it is the selectedDate, for the NumericStepper it is value and for Money it is actually a formated value. This could be annoyingly tedious, but here is where a little simple OOP saves a lot of grief.

What I want to do is to be able to iterate over all of my dynamically generated form items and just grab a common value property. To do this, I need a base class for all of my form inputs that establishes a common getter:

<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml">

<!-- ABSTRACT - NOT FOR INSTANTIATION -->

<mx:Script>
<![CDATA[
    
internal var _formValue:String = "";        
public function get formValue():String {
return _formValue;
}        
        
internal var _formType:String = "";
public function get formType():String {
return _formType.toUpperCase();
}        
]]>

</mx:Script>    
</mx:Canvas>

I also added the formType so that system could distinguish between input types later on. I named this custom class FormInputBase. The thing I was looking to accomplish was to keep the interface for all of the input types exactly the same. So, the five input types extend this base class and add the appropriate input. I use binding to keep the _formValue updated:

FormTextInput:

<?xml version="1.0" encoding="utf-8"?>
<FormInputBase xmlns="types.*" xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="27" creationComplete="_formType='TEXT'">
<mx:TextArea id="txtInput" top="0" bottom="0" left="0" right="0" />
<mx:Binding destination="_formValue" source="txtInput.text" />    
</FormInputBase>

FormTextArea:

<?xml version="1.0" encoding="utf-8"?>
<FormInputBase xmlns="types.*" xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="75" creationComplete="_formType='TEXTAREA'">
<mx:TextArea id="taInput" top="0" bottom="0" left="0" right="0" />
<mx:Binding destination="_formValue" source="taInput.text" />    
</FormInputBase>

FormDate:

<?xml version="1.0" encoding="utf-8"?>
<FormInputBase xmlns="types.*" xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="27" creationComplete="_formType='DATE'">
<mx:DateField id="dfInput" top="0" bottom="0" left="0" right="0" />
<mx:Binding destination="_formValue" source="dfInput.selectedDate.toDateString()" />        
</FormInputBase>

FormNumber:

<?xml version="1.0" encoding="utf-8"?>
<FormInputBase xmlns="types.*" xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="27" creationComplete="_formType='NUMBER'">
<mx:NumericStepper id="nsInput" stepSize="1" minimum="0" maximum="99999999" top="0" bottom="0" left="0" right="0" />
<mx:Binding destination="_formValue" source="nsInput.value.toString()" />        
</FormInputBase>

FormMoney:

<?xml version="1.0" encoding="utf-8"?>
<FormInputBase xmlns="types.*" xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="27" creationComplete="_formType='MONEY'">
<mx:CurrencyFormatter id="usdFormatter" precision="2" rounding="nearest" />
<mx:NumericStepper id="nsInput" stepSize=".01" minimum="0" maximum="99999999" top="0" bottom="0" left="0" right="0" />
<mx:Binding destination="_formValue" source="usdFormatter.format(nsInput.value)" />    
</FormInputBase>

Notice that in the creationComplete that we set the _formType. As mentioned, the Binding bridges the gap to the common property, thus making our lives easier.

Ok, back to the InputDialog and hopefully getting nearer to wrapping this up. In the setter for inputs, we want to clear the display area, add a Form, to gets us a ready-built visual framework, and then loop over the injected array to add the appropriate input elements. I've broken the functionality out into as many small pieces as reasonable to prepare this to be adapted to a particular requirement. So, while looping, I'll call a different function, addNewInput, to handle the actual addition of the input. And, even in there, I'm using a helper function to get each of the input types. Again, not because it is necessary here, but because it might be useful in the future. Each of the input components are added to the Form, so that enables us to iterate over the children of the Form later on.

And, as I try to hasten the end of this post, let's go over the what should happen when the input form is submitted. As mentioned, all of the inputs are added to the Form instance, so that we can just loop over the children, accessible through the getChildren() method. Since we normalized all of the inputs, we can create single flow and prepare an array of values that correspond to the original placeholders. In fact, we will reconstitute the placeholder text to enable the populateTemplate() function in the calling component.

<?xml version="1.0" encoding="utf-8"?>
<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="400" height="300" showCloseButton="true" close="closeMe()">

<mx:Metadata>
[Event(name="WindowClose", type="flash.events.Event")]
[Event(name="InputComplete", type="flash.events.Event")]
</mx:Metadata>    

<mx:Script>
<![CDATA[
import types.FormMoney;
import types.FormNumber;
import types.FormDate;
import types.FormInputBase;
import types.FormTextInput;
import types.FormTextArea;
        
import mx.containers.FormItem;
import mx.containers.Form;
import mx.core.UIComponent;
        
static public const EVENT_CLOSE:String = "WindowClose";
static public const EVENT_COMPLETE:String = "InputComplete";
        
public function set inputs(values:Array):void {
vbInputs.removeAllChildren();
var newForm:Form = new Form;
newForm.percentWidth = 100;
newForm.percentHeight = 100;
newForm.id = "theForm";
newForm.label = "Input Items";
vbInputs.addChild(newForm);
            
for (var i:int=0;i<values.length;i++) {
addNewInput(values[i][1],values[i][2],newForm);
}
            
}
        
private function addNewInput(type:String,label:String,parent:Form):void {
var _tempFormItem:FormItem = new FormItem;
_tempFormItem.percentWidth = 100;
_tempFormItem.label = label;
            
var _formItem:UIComponent;
            
switch (type.toLowerCase()) {
case "textarea":
_formItem = newTextArea();
break;
                
case "date":
_formItem = newDate();
break;
                
case "number":
_formItem = newNumber();
break;
                
case "money":
_formItem = newMoney();
break;

case "text":
default:
_formItem = newText();
break;
}
_tempFormItem.addChild(_formItem);            
                        
parent.addChild(_tempFormItem);
            
            
}
        
private function newTextArea():UIComponent {
var _returnItem:FormTextArea = new FormTextArea;
_returnItem.percentWidth = 100;            
return _returnItem;    
}

private function newText():UIComponent {
var _returnItem:FormTextInput = new FormTextInput;
_returnItem.percentWidth = 100;        
return _returnItem;
}

private function newDate():UIComponent {
var _returnItem:FormDate = new FormDate;
return _returnItem;
}        

private function newNumber():UIComponent {
var _returnItem:FormNumber = new FormNumber;
return _returnItem;
}

private function newMoney():UIComponent {
var _returnItem:FormMoney = new FormMoney;
return _returnItem;    
}

private var _returnForm:Array = [];
public function get formValues():Array {
return _returnForm;
}

private function submit():void {
_returnForm = [];
for (var c:int=0;c< vbInputs.getChildren()[0].getChildren().length; c++) {
var thisItem:FormItem = vbInputs.getChildren()[0].getChildren()[c] as FormItem;
var returnItem:Array = [];
returnItem[0] = thisItem.label;
returnItem[1] = (thisItem.getChildren()[0] as FormInputBase ).formValue;
returnItem[2] = "${INPUT|" +(thisItem.getChildren()[0] as FormInputBase ).formType+ "|" +thisItem.label+ "}";
_returnForm.push(returnItem);
}
throwComplete();
}

private function closeMe():void {
throwClose();
}

private function throwClose():void {
dispatchEvent(new Event(EVENT_CLOSE));
}

private function throwComplete():void {
dispatchEvent(new Event(EVENT_COMPLETE));
}
        
]]>

</mx:Script>

<mx:VBox id="vbInputs" width="100%" top="0" bottom="35"></mx:VBox>
    
<mx:HBox bottom="0" height="27" left="10" right="10">
<mx:Spacer width="100%" />
<mx:Button label="SUBMIT" width="150" click="submit()" />
</mx:HBox>

</mx:TitleWindow>

Here is the complete flex regular expression template application with source .

I skimmed or skipped over a few things in the interest of trying to avoid rambling on forever, so let me know if I didn't cover something important.

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Jonathan's Gravatar Neat example. I can think of many codegen type enhancements. Keep rockin.
# Posted By Jonathan | 12/16/09 8:06 PM
Brandon L.'s Gravatar Hi, thanks for the post. And I watched a related video at http://www.videorolls.com/watch/Pecha-Kucha-Presen... So it might be useful to lots of people.
# Posted By Brandon L. | 7/2/10 7:40 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1.001. Contact Blog Owner