Posted by Kas Thomas AUG 03, 2010
Posted in crx, crx gems, dynamic languages and javascript Add comment
I recently showed a simple way to render CRX content as PDF. The trick is to use a PDF form as a container, into which form data is imported using XFDF (the latter being Adobe's XML Forms Data Format). Populating a PDF form this way is very easy. But the form we ended up with last time was rather static, with no user interactivity to speak of. It simply showed movie data in a predetermined format.
What would be good is if the user could, say, choose a film title from a scrolling list, then see the form update (in real time) with data for the selected film title. This turns out to be fairly easy to do.
In addition to text fields, buttons, checkboxes, and radio buttons, PDF forms support the notion of a list box. A list box can be initialized with
list.setItems( [ [Item1, exportValue1], [Item2,
exportValue2 ] ... ] );
where Item1, Item2, etc. are the names of list items that will be seen by the user and exportValue1, exportValue2, etc. are the corresponding values that get sent to the server in a POST request. This is quite handy, because it means we can arrange things so that if the user selects "Terminator 2" from a list of films, the value that gets sent to the server is "terminator_2.xfdf." This is exactly what we want.
Our new form, containing the list box, looks like this:

The list box (on the right) needs to be initialized with a list of film names (and corresponding export values) at form-load time. For that to happen, we need an onload script that does something like
var list = this.getField( "list" );
list.setItems( movies );
where movies is an array of arrays (containing items and export values) per the previous code snippet (above). The question is how to get the data for movies into the form. Either this information has to be hardcoded into the form to begin with (which is certainly possible), or it has to come from the server. We've seen before that it is easy to pull data into a PDF form with XFDF. In this case, we have a script on the server that is dynamically generating our XFDF. We can set things up so that the script generates the array of movies (and export values) as part of the XFDF.
Unfortunately, XFDF won't populate a list box directly. But what we can do is suck movie data into an invisible text field, then have our onload script slurp the data from the text field and programmatically populate the list box at onload time. The onload script looks like this:
// if hiddenText data is in JSON format . . .
movies = eval( this.getField( "hiddenText" ).value );
// populate the list of movies
var list = this.getField( "list" );
list.setItems( movies.sort( ) );
The Acrobat JavaScript API doesn't directly support a notion of onload events per se, but in practice all we have to do to get a script to execute at load time is attach it to the form as a document-level script. In Acrobat 9 Pro, you can create such a script using the Document JavaScripts command under Advanced > Document Processing.
In addition to the onload script, our PDF form needs one additional script. Underneath the list box, we have a button that says "Click here to view details for the selected movie." When that button is clicked, we want a script to fire. The script will request the XFDF containing film data for the selected film (causing the form to repopulate). This script looks like:
var f = this.getField("list");
var a = f.currentValueIndices;
selection= f.getItemAt(a, true);
getURL( "/content/films/" + selection + ".xfdf" );
The first line simply gets a reference to the list box. The second and third lines fetch the user's selection (whatever is highlighted in the list box). The fourth line sends a properly formed GET request to the server.
Recall from last time that in our repository, we have content nodes under /content/films that look like:

In my earlier post, I presented a script (xfdf.esp) that produces XFDF-formatted movie data. The script looked like this:
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<f href="/apps/films/FilmSummary.pdf"/>
<% response.setContentType("application/vnd.adobe.xfdf" ); %>
<fields>
<field name="Director">
<value><%= currentNode.Director %></value>
</field>
<field name="Subject">
<value><%= currentNode.Subject %></value>
</field>
<field name="Year">
<value><%= currentNode.Year %></value>
</field>
<field name="Title">
<value><%= currentNode.Title %></value>
</field>
<field name="Length">
<value><%= currentNode.Length %></value>
</field>
<field name="Actor">
<value><%= currentNode.Actor %></value>
</field>
<field name="Actress">
<value><%= currentNode.Actress %></value>
</field>
<field name="Popularity">
<value><%= currentNode.Popularity %></value>
</field>
</fields>
</xfdf>
We can shorten this script quite a bit by creating the XML programmatically using E4X:
fields = <fields/>;
names = ["Title","Director","Subject",
"Actor","Actress","Length",
"Popularity","Year"];
for (var i = 0; i < names.length;i++) {
field = <field><value>{currentNode[names[i]]}</value></field>;
field.@name = names[i];
fields.* += field;
}
The second change to the script that we need to make is to add a section that generates the movie list that will go in the form's list box. The data needs to be an array of arrays, in JSON format, of the form [ [Title1, exportValue 1], [Title2, exportValue2] ... ]. Thus, we need to add the following block of code:
parent = currentNode.getParent();
allFilms = parent.getNodes();
json = "[ ";
for ( i in allFilms ) {
nodeName = i;
filmTitle = allFilms[i].Title;
json += "[ \"" + filmTitle + "\",\"" +
nodeName + "\"],"
}
json += "[] ]";
field = <field><value>{ json }</value></field>;
field.@name = "hiddenText";
fields.* += field;
The complete server-side script, xfdf.esp, now looks like this:
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<f href="http://localhost:7402/apps/films/FilmSummary2.pdf"/>
<%
fields = <fields/>;
names = ["Title","Director","Subject",
"Actor","Actress","Length",
"Popularity","Year"];
for (var i = 0; i < names.length;i++) {
field = <field><value>{currentNode[names[i]]}</value></field>;
field.@name = names[i];
fields.* += field;
}
parent = currentNode.getParent();
allFilms = parent.getNodes();
json = "[ ";
for ( i in allFilms ) {
nodeName = i;
filmTitle = allFilms[i].Title;
json += "[ \"" + filmTitle + "\",\"" +
nodeName + "\"],";
}
json += "[] ]";
field = <field><value>{ json }</value></field>;
field.@name = "hiddenText";
fields.* += field;
%>
<%= fields.toXMLString() %>
</xfdf>
And that's all there is to it: two dozen lines of code on the server, and seven lines of code in the PDF form (a 3-line onload script plus a 4-line button click handler). That's all that's required to have an interactive PDF form-driven application on CRX.
Of course, there's still room for improvement. The way things are now, this form reloads and repaints the listbox data as part of every roundtrip to the server. That's not necessarily the worst thing in the world to do (it causes about two seconds of extra latency), but it would be nice if we didn't have to reload all the movie titles with each request. It would also be nice if the user could look up movies based not just on the title, but on the director, year, starring actor, or other criteria. In other words, it would be nice to have a search-driven app. We'll tackle that in a future blog.






