AIR:Articles:Using HTML in Flex-based Adobe AIR Applications
From Adobe Labs
By Edward Mansouri (http://www.airapps.net)
AIR brings developers using Flex the ability to incorporate HTML-based applications into their interfaces. Using the WebKit browser engine, AIR loads HTML into its runtime alongside Flash content creating a hybrid application not possible in browser-based Flex applications. Of even greater significance, using a method known as "Script Bridging", JavaScript in the HTML portion of the application and ActionScript in the Flash portion of the application can exchange information and call one another's methods.
In this article, I will differentiate between two methods of using HTML in a Flex-based AIR application: the HTML component, and the HTMLControl class. Further, I will provide in-depth coverage of Script Bridging, with multiple code-based examples used to develop a simple Flash/HTML hybrid application.
| Table of contents |
Requirements
In order to make the most of this article, you need the following software and files:
Sample files: edward_mansouri_sample_code.zip (628K)
Prerequisites: You will need a working knowledge (intermediate level or above) of Flex Builder 3, HTML, JavaScript.
The HTML component
The simplest way to leverage HTML in AIR applications is with the HTML component (<mx:HTML/>) included with the AIR Flex Builder 3 components. It provides a rectangular HTML rendering UI component that renders HTML code defined as a string within the Flex application, or, a remote or local URL via the component's location property.
The HTML component's properties are set within its MXML declaration, or programmatically with ActionScript.
For instance, the following element will load the Adobe website into an 800 x 600 HTML component:
<mx:HTML id="html" width="800" height="600" location="http://www.adobe.com/">
Using the following ActionScript will accomplish the same results:
html.location = "http://www.adobe.com/";
In addition to rendering HTML from a remote or local URL, you can create a string representing an HTML document in ActionScript and assign it to the HTML component's htmlText property. For example:
var htmlString:String = "<html><body>Hello World</body></html>"; html.htmlText = htmlString;
You can assign a complete HTML document, or an HTML fragment, to the htmlText property. When setting the htmlText property in this manner, it is best to assign width and height properties to the HTML component, otherwise an unexpected layout can occur.
Determining the user's location in an HTML component
The locationChange event is fired when a new URL is contacted in an HTML component. This could occur when a user clicks links embedded within the web page loaded to the component.
The following simple example shows the locationChange event of the HTML component:
<mx:HTML id="html" width="800" height="600"
location="http://www.adobe.com/"
locationChange="showLocation(event);"/>
<mx:Script>
<![CDATA[
private function showLocation(event:Event):void
{
trace("Location: " + event.currentTarget.location);
}
]]>
</mx:Script>
The following example shows a user their current location using an HTML component with a Text component, using the fact that the location property is bindable:
<mx:HTML id="html" width="800" height="600" location="http://www.adobe.com/">:
<mx:Text width="800" text="{html.location}"/>
Using the HTMLControl class
In certain instances, it may be necessary to access the HTML component's htmlControl property to accomplish certain tasks. The htmlControl property is a reference to an HTML component's underlying HTMLControl object. For instance, the HTMLControl object can be used to set or get the scroll position of the parent HTML content.
Also, there are some differences in how certain actions are accomplished with the HTML component versus the HTMLControl object. For instance, to load HTML content into an HTML component, we simply point its location property at a URL. However, with the HTMLControl object, we call its associated load method:
var html:HTMLControl = new HTMLControl();
var req:URLRequest = new URLRequest("http://www.adobe.com");
html.load(request);
When using the HTMLControl class, HTML is loaded into an AIR application by first instantiating an instance of the class, and then calling the load() method (for loading HTML from an URLRequest), or the loadString() method (for loading HTML from a String).
Below is a simple example:
var html:HTMLControl = new HTMLControl();
var req:URLRequest = new URLRequest("http://www.adobe.com/");
html.load(req);
html.width = 800;
html.height = 600;
stage.addChild(html);
A call to the stage object's addChild() method is needed to make the HTML content viewable.
Alternatively, as is the case with the HTML component, an HTML string can be rendered when working with the HTMLControl object:
var html:HTMLControl = new HTMLControl(); var htmlString:String = "<html><body>Hello World</body></html>"; html.loadString(htmlString); html.width = 800; html.height = 600; stage.addChild(html);
The HTMLControl and the HTML component are both Flash DisplayObjects, meaning the properties applied to traditional DisplayObjects including alpha, graphic filters, etc. can be applied.
Script bridging
The power of embedding HTML into AIR applications becomes clear when Script Bridging is investigated.
<math>Script Bridging enables ActionScript code in the Flash portions of an AIR application to communicate and call methods in the JavaScript code in its HTML portions. The reverse is true as JavaScript communicates with and calls methods in ActionScript.</math>
In addition, and perhaps even greater significance, ActionScript code in the Flash portions of an AIR application can inspect and manipulate the DOM elements of its HTML portions.
The implication is that an AIR application can load an HTML-based application and extract parts of its content for use by the Flash, or even rewrite portions of it dynamically.
DOM manipulation
As with a traditional browser-based HTML application, in AIR, an HTML page's DOM elements are not accessible for manipulation and inspection until the page has fully loaded.
In an HTML application, when DOM methods need to be invoked immediately, the body element's onload method is typically used to call the DOM manipulating function. This insures the page has fully loaded, and its DOM elements are available.
The model is similar to an AIR application, as the HTMLControl dispatches a complete event indicating the HTML content has finished loading and the DOM is available for inspection and manipulation. The DOM is made available to us via the JavaScriptObject object.
In the example below, we load a URL into an HTML component, then list the links embedded within the page upon receiving the complete event:
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
xmlns:ns1="http://www.adobe.com/2006/air" applicationComplete="buildListings();" width="800" height="600">
<mx:Script>
<![CDATA[
import flash.html.JavaScriptObject;
private function buildListsings():void
{
html.location = "http://www.adobe.com";
}
private function showLinks(event:Event):void
{
var dom:JavaScriptObject = event.currentTarget.javaScriptDocument;
var links:Object = dom.getElementsByTagName("a");
for(var i:Number = 0; i < links.length; i++)
{
trace(links[i].getAttribute("href"));
}
}
]]>
</mx:Script>
<mx:HTML id="html" x="10" y="10" width="824" height="497" complete="showLinks(event);"/>
</mx:WindowedApplication>
In the above example, the powerful javaScriptDocument property of the HTML component should be noted. This exposes all the HTML content's DOM and JavaScript to ActionScript. Also, the complete event is fired when the HTML content has fully loaded into the HTML component.
As a similar example, with a variation, we will load an HTML page and change all its default fonts. To do this, we'll replace the showLinks() function with the setFonts() function below:
private function setFonts(event:Event)
{
var dom:JavaScriptObject = event.currentTarget.javaScriptDocument;
dom.getElementsByTagName("body")[0].style.fontSize = "18px";
dom.getElementsByTagName("body")[0].style.fontFamily = "Courier";
}
We would also need to change the assignment of the complete event to setFonts(event) in our <mx:HTML/> element.
Keeping track of user actions in HTML from ActionScript
Document-level event listeners can be assigned to the JavasScriptObject as illustrated in the example below, which traces each DOM element in the HTML to receive an onmouseup event.
public function onHtmlComplete(event:Event):void {
var doc : * = event.currentTarget.domWindow;
doc.onmouseup = function():void {
trace(arguments[0].srcElement);
}
}
<mx:HTML id="html" location="http://www.google.com" complete="onHtmlComplete(event);" />
Cross-domain scripting issues
In browser-based applications, the above actions would only be possible with JavaScript making calls from a page loaded to the domain http://www.adobe.com/ due to the web browser's standard security model.
However, you have unrestricted access to manipulate the DOM of any web content rendered into an HTMLControl object.
Calling JavaScript from ActionScript
The HTMLControl has a built-in JavaScript engine that can easily be targeted from ActionScript, bringing a level of JavaScript and ActionScript integration never before possible.
As a simple example of this, we'll develop is a mash-up of the Google Maps API deployed in an AIR application.
Full coverage of building Google Maps applications is outside the scope of this article. For our purposes, it is sufficient to explain that Google Maps provides developers with an API that enables maps to be embedded within an HTML application and via an extensive JavaScript library, the application can manipulate and control the map in numerous ways.
Extensive information is available at http://www.google.com/apis/maps. We will look at such an example centered around an AIR application with mixed Flash and HTML content. The Flash content contains a DataGrid displaying real-estate listings. The HTML content contains a simple Google Maps application. When the user clicks on a listing in the DataGrid, the listing is mapped in the Google Maps application.
Script Bridging and dynamic exchange between ActionScript and JavaScript makes this possible.
The possible sources of data, which will populate the DataGrid, are outside the scope of this article. In our example, we will manually construct an ArrayCollection object with each member representing a single listing with its address, listing price, a URL to a photo of the listing, the listing's latitude and longitude, and its status as either for sale, or sold.
Setting up the application in Flex
Now we will set up the application in Flex.
Using the DataGrid component
We will first drag a DataGrid component onto the stage. Below is its associated MXML whose DataGridColumn children we've manually manipulated:
<mx:DataGrid x="10" y="10" width="780" height="162" id="properties" dataProvider="{propertyCollection}" change="locateHouse(event);"
enabled="false">
<mx:columns>
<mx:DataGridColumn headerText="Address" dataField="address"/>
<mx:DataGridColumn headerText="Listing Price" dataField="listingPrice" width="200"/>
<mx:DataGridColumn headerText="Status" dataField="status" width="100"/>
<mx:DataGridColumn headerText="Photo" dataField="photo" width="100"/>
</mx:columns>
</mx:DataGrid>
The DataGrid's dataProvider property is attached via data binding to propertyCollection, an ArrayCollection built according to the below protocol:
// Use the ArrayCollection versus the Array [Bindable] var propertyCollection:ArrayCollection = new ArrayCollection(); // Use the ObjectProxy versus the Object var thisObject:ObjectProxy = new ObjectProxy(); thisObject.address = "220-D N Cleveland St D Indianapolis, IN"; thisObject.listingPrice = "$266,900"; thisObject.photo = "house1.jpg"; thisObject.lat = 39.770482; thisObject.lon = -86.149335; thisObject.status = "For sale" // Call the ArrayCollection's addItem method propertyCollection.addItem(thisObject);
We use the ObjectProxy versus the Object to populate our ArrayCollection. When binding an ArrayCollection to a UI Control, I recommend using the ObjectProxy whenever changes to the data model need to be rendered in real time.
The ObjectProxy in ActionScript 3.0 enables data binding to occur for individual properties of an ObjectProxy that is a member of an ArrayCollection bound to a UI control. This property-level binding does not occur when using an Object as a member of an ArrayCollection.
We also create an Application instance variable named r. This will be utilized later:
private var r:Object = this;
We will now add an HTML component to the Stage with the ID of html, and position and size it as follows:
<mx:HTML x="10" y="180" width="780" height="400" id="html" exposeRuntime="true" complete="enableDataGrid();" location="{htmlPage}"/>
We assign the exposeRuntime property to true. This enables the JavaScript in the HTML portion of our application to access ActionScript classes.
Our location property is assigned to a variable htmlPage we assign as:
[Bindable] private var htmlPage:String = "file://"+File.appResourceDirectory.nativePath+"/assets/map.html";
Although "assets/map.html" is usually a sufficient URL to pass, in our example, the Google Maps API requires the protocol file:// or http://.
We have an "assets" folder within Flex under the base of our core FLASH resources and all resources related to the HTML portions of the application are in this folder including GIF, JPG, and HTML.
This enables us to bundle HTML applications with our AIR applications without requiring the HTML code to be retrieved remotely.
We have assigned a function named enableDataGrid to the complete event of our HTML component. Further, notice the DataGrid is instantiated with its enabled property set to false. The enableDataGrid function simply sets the enabled property to true:
private function enableDataGrid(event:Event):void
{
properties.enabled = true;
// pass a reference of r to the HTML application
html.javaScriptWindow.r = r;
}
This prevents the user from clicking the DataGrid (which in turn prompts communication with JavaScript in the HTML component), before the HTML component has fully rendered the HTML application. Failure to include this logic could result in runtime errors if ActionScript attempts to access DOM elements or JavaScript objects that have yet to finish loading.
Also, in this function, we pass a reference to our Application instance r to the HTML component's javaScriptWindow object as a JavaScript variable named r. From JavaScript, to access the ActionScript runtime, we simply refer to this established variable, r.
Back to our Flash content, the DataGrid component has a change handler registered -- locateHouse() -- invoked upon the user clicking the DataGrid:
private function locateHouse(event:Event):void
{
var thisItem:Object = event.currentTarget.selectedItem;
var address:String = thisItem.address;
var listingPrice:String = thisItem.listingPrice;
var photo:String = thisItem.photo;
var lat:Number = thisItem.lat;
var lon:Number = thisItem.lon;
var status:String = thisItem.status;
html.javaScriptWindow.locateHouse(lat, lon, address, listingPrice, photo, status);
}
The javaScriptWindow object of our HTML component is what makes communication between ActionScript and JavaScript possible. In the above locateHouse() ActionScript function, a function of the same name is called on the html.javaScriptWindow object with six arguments passed.
The HTML content at assets/map.html (see Figure 1 below) contains the locateHouse() function called in the locateHouse() function in ActionScript and receives as parameters, the six variables that define the real estate listing to be mapped:
function locateHouse(lat, lon, address, price, photo, status)
{
lat = Number(lat);
lon = Number(lon);
var forSaleChecked;
var soldChecked;
if(status == "For sale")
{
forSaleChecked = "checked";
}
else if(status == "Sold')
{
soldChecked = "checked";
}
map.setCenter(new GLatLng(lat, lon), 15);
var myPoint = new GLatLng(lat, lon);
var pointMarker = new GMarker(myPoint, icon);
map.addOverlay(pointMarker);
showHouse(pointMarker, address, price, photo);
pointMarker.openInfoWindowHtml('<div style="font-family: Arial, Helvetica;font-size: 12px;"><span style="font-size: 10px;
font-style:bold;"><u>'+price+'</u></span><br/>'+address+'<p/><img src="'+photo+'" align="left"/><input type="radio" name="status"
'+forSaleChecked+' onclick="changeStatus('+listingId+',\'For sale\')"/> For sale<br/><input type="radio" name="status"
'+soldChecked+' onclick="changeStatus('+listingId+',\'Sold\')"/> Sold</div>');
return pointMarker;
}
Figure 1. HTML example
As mentioned earlier, coverage of the Google Maps API is outside the scope of this article, however, this is a good example of how simple it is to build Google Maps mash-ups.
The full MXML and HTML source used in this example are available for download.
Calling ActionScript from JavaScript
In our HTML application, we created a variable named r. This is overwritten by ActionScript in our enableDataGrid() function and r becomes a reference to the ActionScript runtime.
From enableDataGrid():
html.javaScriptWindow.r = r;
In our HTML application, we display a photo of the property along with two radio buttons to indicate whether the listing is still for sale or if it has been sold. (See Figure 1 above.)
In our example, we will dynamically update the DataGrid in our Flash application by clicking the radio button presented along with the listing.
In the JavaScript locateHouse() function above, notice each radio button has an onclick handler that calls a JavaScript function changeStatus():
function changeStatus(listingId, status)
{
r.changeListingStatus(listingId, status);
}
r in the changeStatus() function above is a reference to the ActionScript runtime. By calling r.changeListingStatus(), we are calling the ActionScript function changeListingStatus() in our Flash content defined below:
public var changeListingStatus:Function = function(listingId:Number, status:String):void
{
for (var i:int = 0; i < propertyCollection.length; i++)
{
if (propertyCollection[i].listingId == listingId)
{
propertyCollection[i].status = status;
}
}
}
Notice the changeListingStatus() function has been defined as public. This is necessary as the JavaScript code in the HTMLControl class is in a different class space.
As the user clicks on the status radio buttons, the DataGrid updates in real time in the Flash portion of the application.
Conclusion
In this article, we reviewed two separate methods of inserting HTML content into an AIR application: the HTML UI component, and the HTMLControl Class. Further, we developed a full example illustrating the powerful Script Bridging capabilities of AIR, and documented techniques for calling JavaScript from ActionScript and ActionScript from JavaScript. With its powerful HTML/Flash integrated runtime, AIR has the potential to redefine the applications that people use to interact with the Internet.
About the author
Edward Mansouri is the owner and founder of Ucompass.com, Inc., an e-learning software company based in Tallahassee, Florida. The company's primary product is a Learning Management System written by Edward called "Educator," which makes extensive use of Flash, Flex, and the Flash Media Server. Edward has been building interactive educational content with Flash since 1998, and Flash applications with Flash 5, Flash MX, Flash MX 2004, Flash 8, and Flex since 2002. Recently, Edward founded AIRApps.net, an interactive online community to help AIR application developers build, market, and sell their applications.

