The same reader who prompted my last diversion (HTML/Adobe AIR Application Diversion One - User Login) also spurred me into another late night of coding. This time his question involved how you could display data loaded via Ajax. While certainly not just an AIR concern (any Ajax application has to worry about this), my experiments focused on the AIR side. Before going any further, let me discuss how I handle this issue now.
I've done "data display" now three ways:
- When I first began doing Ajax devleopment I made use of Adobe Spry. As much as I'm a jQuery fan boy now I've yet to see a JavaScript framework do display of Ajax data as nicely as Spry. It's incredibly simple and practical. I no longer use Spry as I feel more productive in jQuery, but I still think it's a great model.
- More recently I discovered jQuery templates (see my post on it). I really dig this - but it has an issue in AIR. I'm not going to go into why just yet - but it's coming up in my AIR series.
- So outside of the two methods above, most of the time I simply loop over the data, create a big ass string (that's the technical term, honest), and inject it into the DOM.
So that's what I've done so far. I'm sure there are other options, but these have worked great for me so far. My reader though threw me for a loop and asked about doing layout using XSLT. XSLT stands for XSL transformations. I won't go into the complete description here (if you want more detail, see this page), but it's essentially a way to take some XML, combine it with a XML document that describes layout, and generate HTML. It's kinda cool. ColdFusion supports it. But honestly? I've never used it. The thought of using it for Ajax though was quite intriguing.
I did some research and discovered that you can actually do XSLT on the client. This tutorial, XSLT on the Client, talks about how you can load both an XML and XSL file via Ajax and then combine them to create layout. I took their code and simplified it to make it work with AIR. (Since we don't have to worry about IE we can remove some code.)
<html>
<head>
<title>New Adobe AIR Project</title>
<script type="text/javascript" src="lib/air/AIRAliases.js"></script>
<script src="lib/jquery/jquery-1.4.2.min.js"></script> <script>
function loadXMLDoc(dname) {
xhttp=new XMLHttpRequest();
xhttp.open("GET",dname,false);
xhttp.send("");
return xhttp.responseXML;
} function displayResult() {
xml=loadXMLDoc("data.xml");
xsl=loadXMLDoc("test.xsl");
xsltProcessor=new XSLTProcessor();
xsltProcessor.importStylesheet(xsl);
resultDocument = xsltProcessor.transformToFragment(xml,document);
$("#example").html(resultDocument);
} $(document).ready(function() { displayResult(); });
</script>
</head>
<body>
<div id="example"></div>
</body>
</html>
I won't go into detail about this code since it is just a simplified version of the tutorial I linked above, but I think you can get an idea about just how simple this stuff is. Taking this and running it within AIR results in pretty much what you would expect:
Cool, right? XSL is a pretty intense language (syntax? framework? whatever). You can do looping (obviously) and other stuff too - even conditionals. So all of this took just a few minutes. What took me a few hours was deciding to turn this little experiment into a full fledged application.
I began by digging up an XML service. Yahoo has a nice Traffic API that spits out XML so I thought it would be a good starting point. I thought I'd design an application that would prompt you for your address and display the results. Here is a screen shot.
I'll paste the code and - like normal - walk you guys through it. There was some extremely painful parts of this application - mainly because I was unaware of some things that jQuery did for us.
<html>
<head>
<title>New Adobe AIR Project</title>
<script type="text/javascript" src="lib/air/AIRAliases.js"></script>
<script src="/lib/jquery/jquery-1.4.2.min.js"></script>
<script>
//simple func to read in a complete file
function fileRead(fileob) {
var fileStream = new air.FileStream();
fileStream.open(fileob,air.FileMode.READ);
var contents = fileStream.readMultiByte(fileob.size,"utf-8");
fileStream.close();
return contents;
} function lookup(evt) {
var street = $("#street").val();
var city = $("#city").val();
var state = $("#state").val(); if(street == '' || city == '' || state == '') {
alert('Please enter all values.');
return ;
} var url = "http://local.yahooapis.com/MapsService/V1/trafficData?appid=YdnDemo&street=" + escape(street);
url += "&city="+escape(city) + "&state="+escape(state); $.get(url, {}, function(res,status) {
$("#log").html("");
var resultDocument = xsltProcessor.transformToFragment(res,document);
$("#display").html(resultDocument);
}); } $(document).ready(function() {
var xslFile = air.File.applicationDirectory.resolvePath("design.xsl");
var xsl = fileRead(xslFile);
xsl = (new DOMParser()).parseFromString(xsl, "text/xml"); xsltProcessor=new XSLTProcessor();
xsltProcessor.importStylesheet(xsl); $("#trafficBtn").click(lookup); $('#log').ajaxError(function(e,xhr) {
var msg = xhr.responseXML.getElementsByTagName("Message")[0].firstChild.textContent;
$(this).text(msg);
}); });
</script>
</head>
<body> Enter your street: <input type="text" id="street"><br/>
Enter your city: <input type="text" id="city"><br/>
Enter your state: <input type="state" id="state"><br/>
<input type="button" value="Get Traffic Report" id="trafficBtn">
<div id="log"></div>
<p/> <div id="display"></div> </body>
</html>
Ok, so we can pretty much ignore the HTML at the bottom. It's basically a form, a div used for status messages (log), and a display area. Let's go to the fun code.
When the document loads, I begin by loading in my XSL sheet. Notice the use of File.applicationDirectory. This basically means "same place as my app". The fileRead function simply handles doing a synchronous file read using AIR File APIs. I convert my XSL text into a proper XML document. (Don't ask me how I learned that line - thank Google.) I then create my XSL processor object and pass in the XSL. Ignore the error handler for now.
So now let's look at the form handler. I start off grabbing my form values and doing a quick validation on them. Next I create my URL. This just follows the API directions from Yahoo. Finally I fire off the request and when I get my data back, I pass it to my XSL processor. That's it! So what took me so long? I struggled for quite some time because I didn't realize that jQuery had recognized the XML and turned it into an XML object for me. My original code was "re-XMLing" the result. Ugh. (And let me just say. This is the second time in two weeks I've done XML in JavaScript and so far I fracking hate it. ColdFusion handles XML much nicer!) So what about the XSL?
<xsl:template match="/">
<html>
<body>
<h2>Traffic Report</h2>
<table border="1" width="100%">
<tr bgcolor="#9acd32">
<th>Type</th>
<th>Incident</th>
</tr>
<xsl:for-each select="//[local-name()='Result']">
<tr>
<td><xsl:value-of select="@type"/></td>
<td><xsl:value-of select="//[local-name()='Title']"/></td>
</tr>
<tr bgcolor="yellow">
<td colspan="2">
<xsl:value-of select="//[local-name()='Description']"/>
<br/><b>Severity:</b>
<xsl:choose>
<xsl:when test="//[local-name()='Severity']='1'">
Not very
</xsl:when>
<xsl:when test="//[local-name()='Severity']='2'">
Mildly Annoying
</xsl:when>
<xsl:when test="//[local-name()='Severity']='3'">
Annoying
</xsl:when>
<xsl:when test="//[local-name()='Severity']='4'">
Pain in the Rear
</xsl:when>
<xsl:when test="//[local-name()='Severity']='5'">
You ain't going nowhere
</xsl:when>
<xsl:otherwise>
Unknown. Consult Fringe department.
</xsl:otherwise>
</xsl:choose>
</td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
This was also a bit painful, but mainly because of the namespaces. Everywhere you see: //*[local-name() you can thank namespaces. I wish there was a way to ignore them. If a tag is <foo> then I want my path to just use foo. Google seemed to imply that it may be possible to tell the XSL processor to ignore the namespaces but I wasn't able to get it to work. It's not horrible - but I think it makes the XSL harder to read.
Anyway - most of the above is just the loop. The only "fancy" part is my conditional. As you can see - I do some basic branching based on the severity of the traffic report. I could have used graphics instead of text, but I think you get the idea.
So... what do you think? If you want to play with this application I've attached it to the blog entry. Unfortunately Yahoo didn't seem to have traffic data for my home town, but perhaps that's because of the overabundance of swamp, vampires, and werewolves. Your results may vary.