ok, I think I figured it out
the different behavior of ng-init in outer/inner els arises because of the way Angular executes its compiling phase. compiling consists of different steps. the most relevant in this case are:
- controller instantiation
- prelinking
- linking
- postlinking
that take place in this order on a per-DOMnode basis (i.e. for each node, the controller code, if present, is executed before any prelink, link, or postlink f)
ng-init registers a pre-link f on the node it is specified in, which $evals the directive's content (in my example, the f assigns a value to the foo prop). so, when the controller code for the same node is executed, the prop does not exist yet, which is in line with @Aron's answer
in the compile phase, Angular traverses the DOM from the root down on a depth-first basis, which means that parent els are compiled before their children. putting the ng-init directive in an outer el allows the controller of the child node to inherit the outer's scope. this explains the 'outer el' hack
the hack @Aron points to registers an observer on the prop, so that, when the prop is finally $evaluated in the prelink phase, the callback f can find it
I suggest two other possible hacks based on asynchronous JS and Angular features (see this jsFiddle). one involves using setTimeout JS native f, whereas the other is more 'Angular' and resorts to $evalAsync
imho, there's a flaw in Angular's implementation of the ng-init directive with respect to the declared intent. I have hacked the Angular's code to experiment a diverse implementation. It is not difficult (2 lines of code added, even before possibly removing the ng-init directive native code), and works when applied to the code in the jsFiddle above, but I have not tested it on complex apps. For those interested, here is what I'm doing (refs are to v 1.2.0-rc2):
- in the
applyDirectivesToNode f block I declare a non-initialized nodeHasInitData local var
- in the same f, after the local
directiveName var is assigned the directive.name prop value, I test it against the "ngInit" static string, which is the normalized name Angular assigns to the ng-init directive when it is declared on the node
- if the test passes, I set the
nodeHasInitData to true. nothing is done if the test fails (-> nodeHasInitData remains undefined in the closure)
- in the
nodeLinkFn f block, before the if block that checks for the presence of controllers in the node (step 1 in the list above), I'm adding a test on the value of nodeHasInitData (I can do that because nodeLinkFn is defined inside applyDirectivesToNode)
- if the test passes, I invoke
scope.$eval(attrs.ngInit), which is what the prelink f of the native ng-init directive does. both scope and attrs are native params of nodeLinkFn, so they are available. nothing is done if the test fails
- this way, I have moved1 the initialization from step 2 to a new step 0, which feeds the inner el scope before the corresponding controller's code is executed
1. Actually, I have replicated it, because the prelink f defined by the ng-init directive is still there. It is not a great deal, however, and I think it could be easily avoided by changing/removing the directive object
EDIT
To avoid replication, it is safe, for the illustrative purposes of the hack described above, to replace the assignment code of the ngInitDirective Angular var with var ngInitDirective = valueFn({});