Having fun with ActionScript3, E4X and XPath

I admit, I was still a bit wet behind the ears when it came to working with XML in AS3 – never did more then getting some images for a slideshow or loading some config-stuff out of a tiny 6-lines XML document. I thought, working with XML in Flash is still some myXML.firstChild.firstChild.firstChild.nodeValue bulls**t.

For a recent project, I had the chance to actually dive into this E4X magic and do the tricks. And yes, although I would still prefer using everything as serialized AMF, working with XML in Flash makes sense … now. And it’s definitely fun.

The guys from memorphic wrote a great AS3 XPath library. Download it here.

Working with optional nodes and not-known depths

For most cases, the E4X implementation works perfectly well. However, it gets difficult when you are dealing with nodes that might or might not exist. Example coming up next:

<data>
  <group>
    <id>first</id>
    <field>
      <id>firstname</id>
    </field>
    <field>
      <id>lastname</id>
      <errorId>001</errorId>
    </field>
  </group>
  <group>
    <id>second</id>
    <field>
      <id>email</id>
    </field>
    <group>
      <id>address</id>
      <field>
        <id>street</id>
        <errorId>001</errorId>
      </field>
      <field>
        <id>zip</id>
        <errorId>002</errorId>
      </field>
    </group>
  </group>
</data>

With AS3 you cannot check against optional nodes:

trace(xdata..field.(errorId == "001").id); 
// throws an exception, since errorId exists only in some nodes

The XPath implementation from memorphic is working just fine:

var xpq : XPathQuery = new XPathQuery("/");
xpq.path = "//field[errorId = '001']/id";
trace(xpq.exec(xdata));

Output:
<id>lastname</id>
<id>street</id>

Now I want to know every group that contains an error – regardless of the depth.

// get me all group ids that contain error(s) regardless of the depth of the error
var xpq : XPathQuery = new XPathQuery("/");
xpq.path = "//group[*//errorId]/id";
trace(xpq.exec(xdata));

Output:
<id>first</id>
<id>second</id>
<id>address</id>

Why all this stuff? I need to know the first hierarchy group (… they’re representing pages in my project) that contains errors (somewhere in the depth of this group). Here we go:

// get me first hierarchy group that contains error(s)
var xpq : XPathQuery = new XPathQuery("/");
xpq.path = "/data/group[*//errorId][1]/id";
trace(xpq.exec(xdata));

Output:
first

Get the XPath

Although it is obviously a bad habit, there can be nodes, that have non-unique ids. In case you need to save a path to that node, you need to get the XML path (literally the XPath).

<data>
  <group>
    <id>taf1</id>
  <field>
    <id>tellafriend_name</id>
    <label>name</label>
  </field>
  <field>
    <id>tellafriend_email</id>
    <label>email</label>
  </field>
</group>
  <group>
    <id>taf2</id>
  <field>
    <id>tellafriend_name</id>
    <label>name</label>
  </field>
  <field>
    <id>tellafriend_email</id>
    <label>email</label>
  </field>
</group>
</data>
for each(var field:XML in xdata..field)
{
    trace(XPathUtils.findPath(field, xdata));
}

Output:
/data/group[1]/field[1]
/data/group[1]/field[2]
/data/group[2]/field[1]
/data/group[2]/field[2]

However, the nodes have to be somehow different from each other, i.e. by having different parent nodes (like in the example) or different child nodes / values. If this is not the case, it’s not working. Example:

<data>
<field>
  <id>tellafriend_name</id>
  <label>name</label>
</field>
<field>
  <id>tellafriend_name</id>
  <label>name</label>
</field>
<field>
  <id>tellafriend_name</id>
  <label>name</label>
</field>
</data>

Output:
/data/field[1]
/data/field[1]
/data/field[1]

Getting node index

Sorry, this is not possible directly with AS3 E4X or XPath. You need to have a helper function for that.

<data>
  <field>
    <id>testfield1</id>
  </field>
  <field>
    <id>testfield2</id>
  </field>
  <field>
    <id>testfield3</id>
  </field>
</data>
var myfield:XML = xdata.field.(id == "testfield2")[0];
var fields:XMLList = xdata.field;
            
for (var i : int = 0; i < fields.length(); i++) 
{
    if(myfield == fields[i])
    {
        trace("found "+i);
    }
}

Please correct me, as I said before, I’m new to this stuff… ;)

cheers

Comments