2015-04-28

Nested predicates in XPath and the need (or lack of) to reference an outer context from within

Key message: At least in my case, the depth cross-reference ended up not being needed at all. I could come up with a logic where the nested tests cascaded perfectly without any cross-reference from within to the outer context.

Given the following XML:

<Root>
  <Order ID="o1">
    <Purchase ID="p1"/>
    <Invoice ID="i1">
      <Item ID="it1" PurchaseID="p1"/>
      <Transaction ID="t1" ItemID="it1"/>
    </Invoice>
    <Invoice ID="i2">
      <Transaction ID="t2" ParentID="t1"/>
    </Invoice>
  </Order>
  <Order ID="o2">
    <Purchase ID="p2"/>
    <Invoice ID="i3">
      <Item ID="it2" PurchaseID="p2"/>
      <Transaction ID="t3" ItemID="it2"/>
    </Invoice>
  </Order>
</Root>

I needed to select all Transaction nodes from the Order with a Purchase whose ID was "p1".

In this example it should return two nodes, but note that the second node is farther away from the Purchase ID than the first one. In fact, it requires passing through the first node to get to it.

The XPath that solves this riddle is:

1: /Root/Order/Invoice/Transaction[
2:     @ItemID=../Item/[@PurchaseID="p1"]/@ID
3:   or
4:     @ParentID=../../Invoice/Transaction[
5:         @ItemID=../Item[@PurchaseID="p1"]/@ID
6:     ]/@ID
7: ]

In line 5, the XPath gets into a predicate depth of 3 levels, each one selecting the appropriate node from within to out.

2015-04-17

Multiple Directive Resource Contention

Key message: this error can be caused by the declaration code (app.directive("customDir", ...)) being executed more than once, even if it exists only once in your webpage, due to tab/wizard libraries.

Now the long story.

I've been dealing with integrating new features done with AngularJS into a "legacy" application that makes heavy use of jQuery and some tab/wizard library.

The latest riddle I faced was an interesting AngularJS error:

Error: $compile:multidir
Multiple Directive Resource Contention

Multiple directives [customDir, customDir] asking for new/isolated scope on: 

Basically, a custom directive I created that was supposed to be reusable was giving me trouble apparently due to its isolated scope's nature.

Note that the two directives in the error are actually the same. The HTML linked to the error looked like this:

<customdir someparam=""></customdir>

The directive declaration looked like this:

// app is the object returned by angular.module()
app.directive("customDir", function() {
    return {
        restrict: "E",
        scope: { someParam: "=" },
        template: "
...
"
    };
});


What didn't make sense in my case was the set of conditions I was in:

  • Angular code used as a plug-in module: the declarations, templates and HTML needed to use the code were self-contained, meaning that all it needed to be used was hosted in its own single file. To use it, I included the file in the exact place in the webpage where I wanted to use it.
  • AngularJS initialized manually (angular.bootstrap)
  • Isolated scope, but...
  • "Element"-only directive, not attributes within one same DOM element.
  • Only one declaration in the webpage.
  • Dynamic template created from string: the template couldn't be referenced multiple times since it was created from string.
So none of the similar situations I found on the web that ended up with this error matched my case.

During my investigation, following AngularJS's initialization I found out that my code declaration was ending up twice in Angular's execution queue.

Further investigation revealed that the original JS code was being duplicated by the tab/wizard library and ended up being executed twice when the "tab" got active. This happened because the declaration code appeared within the scope of the tab where the Angular logic would be needed.

At this stage I saw two options. Either (1) split the code in two so that the declarations could be included somewhere outside the reign of the tab/wizard region or (2) find a way to forbid the declaration to run more than once.

I didn't want to go for (1) because it would break the self-contained nature of the module. So now I needed to find the most proper way to go with (2).

I looked for an AngularJS API that would allow me to check if directives had already been declared, but couldn't find anything.

So I ended up with a little hack. I used a custom "window" attribute that indicated if my declaration code had already been executed or not. If the attribute was not present, the code would be executed. If it was present, the code would not execute.

The end result looked like this:

if (!window.customDirDeclared) {
   // app is the object returned by angular.module()
   app.directive("customDir", function() { ... });
   window.customDirDeclared = true;
}