Callback filter functions in E4X

Tags:

While porting some xpath-heavy AS2 code to AS3, I ran into problems. E4X (new and improved xml support in AS3) seems to make xpath obsolete. However, all the E4X query examples in the docs use hard-coded literals. I can't use literals because I don't know at code-time what the queries are going to be. My AS2 code creates xpath paths dynamically from variables. What is the E4X equivalent?

After some initial confusion on my part, I now get it. In retrospect, what's below seems obvious -- but that's the way it always is.

Selecting a variable-named node type

Let's work with this xml:

<animals>
  <animal type='baboon' hairColor='auburn'>
     <fav>
        <food name='pizza' />
        <color name='blue' />
        <activity name='combing' />
     </fav>
  </animal>
  <animal type='baboon' hairColor='blond'>
     <fav>
        <food name='donuts' />
        <color name='green' />
     </fav>
  </animal>
  <animal type='elephant' hairColor='bald'>
     <fav>
        <food name='peanut butter' />
        <color name='beige' />
        <activity name='sneezing'/>
     </fav>
  </animal>
</animals>

To find out the the auburn-haired baboon's favorite food, the e4x query is:

var result:XMLList = 
  animals_xml.animal.(@name=='baboon' && @hairColor=='auburn').fav.food.@name;

Now, let if the favorite grouping is a variable, we replace the dot syntax with good, old array/bracket syntax. This is the normal way of accessing variable-named properties on an object.

var favGroup:String = "color";
var result:XMLList = 
  animals_xml.animal.(@name=='elephant').fav[favGroup].@name;

Just be careful with the dots. Notice there is no dot before the opening bracket.

Variables in a filter

Let's variable-ize this hard-coded query:

var result:XMLList = 
   animals_xml.animal.(@hairColor=='blond').fav.color.@name;

and make the attribute and value variables.

var attr:String = "hairColor";
var val:String = "blond";
var result:XMLList = 
   animals_xml.animal.(attribute(attr)==val).fav.color.@name;

Each dot separates a step. Each step has an input and an output XMLList. If the step is a node type, the output list will be a subset of the input list's children, i.e. child nodes with that node type.

If the step is an expression in parentheses, the output list is a filtered subset of the input list itself (not it's children).

If the expression in the parentheses evaluates to true, the 'current node' will be included in the output XMLList. The current node is an XML object. This expression is evaluated once for every node in the incoming XML list.

Note: you might think that you can refer to the current node as 'this'. You can't. 'this' is scoped to the containing code. To get at the current node, use valueOf() (a function of the XML class). We'll see it in action below.

BTW, when I first saw this XML.valueOf function in the docs, I could not figure out why on earth you would ever need a function that returns the object itself. Seemed pointless. But now I know.

Calling custom functions

We are not limited to simple comparisons. This example in the docs gives us a hint of what's possible:

x.employee.(position.toString().search("analyst") > -1)

It is possible to do more than simple comparisons of attributes and node names to values.
We can create custom functions and give them the current node to decide whether to keep that node. For example:

function f(node:XML, color:String):Boolean
{
  return node.@hairColor.toString().toUpperCase() ==
         color.toUpperCase();
}

var result:XMLList = 
  animals_xml.animal.(f(valueOf(),'blond').fav.activity.@name;

Of course, the above example is trivial and you would not really implement it that way. It would be better to simply compare the attribute with a value. But using a custom filter function opens infinite possibilities. For example, take a look at this mxml file.

And here's the result:

Replacement image.
song xml:
<songs>
  <song title="My Dog has Fleas" artistID="100"/>
  <song title="I Hate Wet Socks" artistID="101"/>
  <song title="Hello to the Nice People" artistID="201"/>
  <song title="Moldy Baloney" artistID="202"/>
  <song title="Rainy Days" artistID="203"/>
  <song title="Cloudy Days" artistID="100"/>
  <song title="Mooney Nights" artistID="201"/>
  <song title="Sunburned Noggin" artistID="202"/>
</songs>

artist xml:
<artists>
  <artist id="100" firstName="Sue" lastName="Jones"/>
  <artist id="101" firstName="John" lastName="Ambercrombie"/>
  <artist id="201" firstName="Ed" lastName="Fish"/>
  <artist id="202" firstName="Jane" lastName="Wolf"/>
  <artist id="203" firstName="Mary" lastName="Birch"/>
</artists>

E4X Query using custom function:
songs2XML.song.(songsByArtistAlphaRange(valueOf(),'A','G'))

Result xml:
<song title="I Hate Wet Socks" artistID="101" artistName="John Ambercrombie"/>
<song title="Hello to the Nice People" artistID="201" artistName="Ed Fish"/>
<song title="Rainy Days" artistID="203" artistName="Mary Birch"/>
<song title="Mooney Nights" artistID="201" artistName="Ed Fish"/>

Songs by Artists A-G:
John Ambercrombie: I Hate Wet Socks
Ed Fish: Hello to the Nice People
Mary Birch: Rainy Days
Ed Fish: Mooney Nights

Here, we're using a second xml file to affect the query of our first xml file. Basically, we're linking database tables. I think that's cool.

Port of XPathAPI

Before I came to my senses on how this worked, I actually ported the XPathAPI code to ActionScript 3 and used it in my project. After I figured out how E4X really works, I went back to my code to remove the XPath code.

But I found that in some cases, I liked the xpath code better. The way I wrote the original code -- lots of string manipulation of the xpath paths, arrays of these strings, etc. -- it just turned out that way and using E4X navigation was actually more awkward. So I left the XPathAPI code in.

In case you find yourself in a similar situation, here's the ported XPathAPI code. You might find this useful when porting AS2 to AS3.

Comments

Wow, custom function is really cool !

Just what i was looking for. I'm adding your blog to the resources on my post on E4X here

http://raghuonflex.wordpress.com/2007/04/05/whats-the-big-deal-with-e4x-...

I'm glad you found this helpful. Thanks for the link.

var result:XMLList =
animals_xml.animal.(@name=='baboon' && @hairColor=='auburn').fav.food.@name;

Should be...

animals_xml.animal.(@type=='baboon' && @hairColor=='auburn').fav.food.@name;

Windows: "Where do you want to go today?"
Linux: "Where do you want to go tomorrow?"
FreeBSD: "Are you guys coming, or what?"

thanks

Greetings:
I'm coming from a Mozilla/JavaScript 1.7 environment; so E4X is important for me as well. I have the same concern about using VARIABLES. I would like to know the correct synax for accessing ELEMENTS using a VARIABLE.

One classic need: to check for the existance of a particular ELEMENT using a VARIABLE that contains the ELEMENT's name.

After fiddling with your example and remembering about not using the '.' before the square brackets, I came across the following syntax:

var attr = "hairColor";
var val = "blond";
var creature = 'animal';
var x=...
var y = x[creature].(@[attr]==val).fav.color.@name);

Here, y = green.

Joel, you read my mind!
Great article!

Windows: "Where do you want to go today?"
Linux: "Where do you want to go tomorrow?"
FreeBSD: "Are you guys coming, or what?"

Your article is PACKED with very import info for me.
There's the search() under the 'custom function' section (and the variables part) that is germane to what I need.

After some fiddling, the following is a useful snippet to check for the existence of a particular element that I need to know (using your animals list):

x.toXMLString().search('animal');

...which returns 1; found.

x.toXMLString().search('ric');

...which returns -1; not found.

Windows: "Where do you want to go today?"
Linux: "Where do you want to go tomorrow?"
FreeBSD: "Are you guys coming, or what?"

Only thing to be careful: If the word 'animal' or 'ric' is anywhere else in the xml -- maybe as an attribute value (or part of an attribute value -- e.g. difficulty="tricky") or in a text node, your search will return true even though that might now be what you want.