XAP File lister - 05/02/2008
Today, I'm going to show you how I created a XAP file lister like the one I did in plain old AJAX a few years back. Before we begin, you need to know how to create a functioning Hello World application in XAP, and how to display a list of files in PHP
Tip: Speeding up XAP
First off, I'd like to share a snippet of PHP I used to speed up load times. I added a php file to compress xapcore on the server side. This is an extremely simple two-line bit of code, and if you're using PHP with XAP, I encourage you to do it.
<?php
ob_start('ob_gzhandler');
readfile('xapcore.js');
?>
The first line creates an output buffer that gzips the output when flushed. The second line outputs the xapcore.js file. This is advantageous to sending an inflated gzip to the client by default because with this, the server will verify that the client can handle gzipped data before transferring the file, and send uncompressed data if the client can't handle it.
NOTE: Do NOT parameterize the 'xapcore.js' filename without using very strict validation on the server side. That code would be bad, BAD, BAD:
<?php
ob_start('ob_gzhandler');
readfile($_GET['javascript_file']);
?>
Don't do this. Why? Because then the user could request something like javascript_compressor.php?javascript_file=database.php to get your database connection passwords, or find a back door that would let him execute scripts on your server. Then you'd be SCREWED.
Onward to the XAL.
The file lister XAL file
The first file requested is file_list2.php?start=true&dir=./apache-xap-ajax-framework-0.5.0-incubator/ . The 'start' parameter is there to tell the script that it needs the inital XAL to load, not the XModify code it will be needing later on. The 'dir' parameter is the directory we wish to view. And here's the XML we get back:
<xal xmlns="http://openxal.org/ui" xmlns:xm="http://openxal.org/core/xmodify"> <mco xmlns="http://openxal.org/core/mco" id="lister" class="lister" src="lister2.js"/> <table width="600px" height="250px" onExpand="mco:lister.openDir(event)" expandedImage="images/openfolder.gif" collapsedImage="images/closedfolder.gif"> <column width="277px" cursor="pointer" color="blue"> <header text="File Name"/> </column> <column width="100px"> <header text="Size"/> </column> <column width="100px"> <header text="Type"/> </column> <column width="120px"> <header text="Modified"/> </column> <row path="/file_list2.php?dir=./apache-xap-ajax-framework-0.5.0-incubator/dist&id=flb4d0c41471ef9f3fb660555374cde6c4" id="flb4d0c41471ef9f3fb660555374cde6c4"> <cell text="dist" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/dist'"/> <cell text="-"/> <cell text="Directory"/> <cell text="2008-04-21 10:35:37"/> <row> <cell text="Loading..."/> <cell/> <cell/> <cell/> </row> </row> <row path="/file_list2.php?dir=./apache-xap-ajax-framework-0.5.0-incubator/docs&id=fl3d64c4c4b29722845ca76baa7210c7c9" id="fl3d64c4c4b29722845ca76baa7210c7c9"> <cell text="docs" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/docs'"/> <cell text="-"/> <cell text="Directory"/> <cell text="2008-04-21 11:01:03"/> <row> <cell text="Loading..."/> <cell/> <cell/> <cell/> </row> </row> <row path="/file_list2.php?dir=./apache-xap-ajax-framework-0.5.0-incubator/samples&id=fl1b748aec8c614c34e60d26bf3708b5f2" id="fl1b748aec8c614c34e60d26bf3708b5f2"> <cell text="samples" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/samples'"/> <cell text="-"/> <cell text="Directory"/> <cell text="2008-04-21 11:27:29"/> <row> <cell text="Loading..."/> <cell/> <cell/> <cell/> </row> </row> <row> <cell text="LICENSE.txt" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/LICENSE.txt'"/> <cell text="50747 Bytes"/> <cell text="TXT"/> <cell text="2008-04-21 11:01:05"/> </row> <row> <cell text="NOTICE.txt" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/NOTICE.txt'"/> <cell text="1447 Bytes"/> <cell text="TXT"/> <cell text="2008-04-21 11:01:06"/> </row> <row> <cell text="README.txt" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/README.txt'"/> <cell text="9251 Bytes"/> <cell text="TXT"/> <cell text="2008-04-21 11:01:08"/> </row> <row> <cell text="index.php" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/index.php'"/> <cell text="654 Bytes"/> <cell text="PHP"/> <cell text="2008-04-21 11:33:43"/> </row> </table> </xal>
And that's it. Let's take a look at what the different parts do...
Code breakdown
<xal xmlns="http://openxal.org/ui" xmlns:xm="http://openxal.org/core/xmodify"> ... </xal>
This gives the namespaces for the parser. XAL namespaces are always either http://openxal.org/ui or http://openxal.org/ui/html for widgets and interface, or http://openxal.org/core/*****, where ***** is 'macro', 'mco', 'xmodify', 'data', or a few other things. The xmodify namespace is given but not used yet. You'll see it later on.
<mco xmlns="http://openxal.org/core/mco" id="lister" class="lister" src="lister2.js"/>
MCOs (Managed Client Objects) are how XAP allows us to attach more complex client-side code to our applications. Macros, while robust, can't do much of the computation that might be needed. The 'id' and 'class' attributes are used to create a mapping between an object referenced in the XAL and the javascript class loaded by the 'src' attribute. 'src' can be left blank, if desired, and XAP will try to load the class from the local directory. More on the MCO will be shown below.
<table width="600px" height="250px" onExpand="mco:lister.openDir(event)" expandedImage="images/openfolder.gif" collapsedImage="images/closedfolder.gif"> ... </table>
The table widget in XAP is a very robust element. It has column sorting, expandable rows, attributes for styling, and events for table actions such as expanding rows and sorting columns. This means that most of the code is already written for us, and we just need to plug in a few attributes. The 'onExpand' attribute triggers an event handler any time a row within the table is expanded. We call the onExpand event in the MCO, and pass the event object, which contains information about the clicked row. The 'expandedImage' and 'collapsedImage' determine what icon is displayed to next to folders.
<column width="277px" cursor="pointer" color="blue"> <header text="File Name"/> </column> <column width="100px"> <header text="Size"/> </column> <column width="100px"> <header text="Type"/> </column> <column width="120px"> <header text="Modified"/> </column>
One way that XAL tables differ from HTML tables is that they have column declarations at the top. This allow us to apply styling down a whole column (in this case, cursor and font color), and more easily change column width, as well as have better control over column sorting. Doing a replaceChildren call on the table doesn't replace the columns and their headers, which I find to be rather unintuitive and frankly wrong, but that's the behavior. Table tend to behave erratically when columns are added and removed. We also set the width of all the columns so that a long filename won't make the first column too wide.
<row path="/file_list2.php?dir=./apache-xap-ajax-framework-0.5.0-incubator/dist&id=flb4d0c41471ef9f3fb660555374cde6c4" id="flb4d0c41471ef9f3fb660555374cde6c4"> <cell text="dist" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/dist'"/> <cell text="-"/> <cell text="Directory"/> <cell text="2008-04-21 10:35:37"/> <row> <cell text="Loading..."/> <cell/> <cell/> <cell/> </row> </row>
There's a lot of meat in these few lines. I'm going to break it down a little more
<row path="/file_list2.php?dir=./apache-xap-ajax-framework-0.5.0-incubator/dist&id=flb4d0c41471ef9f3fb660555374cde6c4" id="flb4d0c41471ef9f3fb660555374cde6c4"> ... </row>
The path of the row is used by the MCO to request the subrows of the current row. It contains the path of the directory which is being expanded, and the id of the directory, so the application knows where to append the rows it receives. There will be more on that later. The id is generated with a hash function.
<cell text="dist" textDecoration="underline" onMouseDown="javascript:window.location.href='http://www.trevoroldak.com/apache-xap-ajax-framework-0.5.0-incubator/dist'"/> <cell text="-"/> <cell text="Directory"/> <cell text="2008-04-21 10:35:37"/>
We add some functionality here that my original file lister didn't have. Now, when you click on a row's expand/collapse image, it will take you to that directory in your browser. Clicking on the image expands that row. That way, you can go to a directory's index page if you want. Note: GoDaddy will give a 404 error if an index.html or index.php file doesn't exist, so some directories in my lister give a File Not Found error. We set the underline of the cell text in the cell, rather than the column, because of a minor bug that does some unnecessary text underlining if it is set in a column.
<row> <cell text="Loading..."/> <cell/> <cell/> <cell/> </row>
This is a dummy row put in here to make it's parent row expandable. When the row is expanded, the onExpand event event in the table is fired, which retrieves and processes a XAL file from the server, which has rows to replace this dummy row.
So, we've seen our XAL, let's see how the application calls the javascript, and the javascript retrieves more XAL from the server. Like I showed you above, the MCO tag in the page opens a javascript file that assist with our application. Here's the javascript file:
The MCO
lister = function(){};
lister.prototype.openDir = function(event){
var doc = event.source.ownerDocument;
event.session.getRequestService().retrieveAndProcess(doc.getElementById(event.rowId).getAttribute('path'));
}
That's the whole thing. We create a javascript Object, and give it a method. The method takes and ID, gets the row that was the source for the event, and gets its path. Then, it calls retrieveAndProcess on the path, which requests XAL from the server and reads it, just like it did the first file listing. XAL has XMOdify, which has a lot of the DOM manipulation functionality of DHTML. This means that the processed XAL can use XModify to replace rows in the table, such has the placeholder child of the row we just expanded.
The requested XAL
Let's take a look at the file requested when we expand a row. Here's the output from
<xal xmlns="http://openxal.org/ui" xmlns:xm="http://openxal.org/core/xmodify">
<xm:modifications>
<xm:replace select="id('flb4d0c41471ef9f3fb660555374cde6c4')/row">
...
<i>[A whole bunch of row XAL, like you saw above]</i>
...
</xm:replace>
</xm:modifications>
</xal>
The first row is the same as before. The second one indicates that there's some XModify enclosed as child nodes, and then there's the replace tag. The replace tag uses an XPath statement to say which element in the XAL DOM is being replaced. It can also be used on the HTML DOM, but that's for another lesson. We replace the dummy row child of the parent node with XML describing its folder contents. And ta daaaaaa, we have a file lister.