Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ng-init + ng-controller: strange behavior in the controller's scope

I'm new to Angular but really enjoying its approach. I have an HTML file where I am initializing a variable with ng-init in a <div> element, where I'm also declaring a controller with the ng-controller directive:

<div ng-controller="myCtrl" ng-init='foo="bar"'>

If I console.log the $scope object from the controller script I can see the foo property listed among the others, but when I try to access it from the same script it gives me undefined. I'm also using Batarang and it shows me a model for the <div>-scope that also includes the foo property.

I know from the second answer to Pass variables to AngularJS controller, best practice? that I can solve the problem by moving my ng-init directive into an outer <div>, but I would like to know what is really going on here behind the scenes. Any help greatly appreciated, thanks in advance.

EDIT

The order of the directives in the div element does not matter. The problem is still there even if ng-init is specified before ng-controller

like image 739
tetotechy Avatar asked Jun 14 '13 08:06

tetotechy


2 Answers

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:

  1. controller instantiation
  2. prelinking
  3. linking
  4. 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({});

like image 93
tetotechy Avatar answered Oct 12 '22 11:10

tetotechy


the controller gets created first and then foo=bar is set.

what this means is that in the code of your controller, foo doesnt exist yet. however when you are debugging, it has already been created.

here is a way around it, but i think it is really a hack. Why is the variable set in ng-init undefined in $scope in AngularJS?

imho the angular way to fix this is not put data into the view. Data belongs in your model or controller or service. This is to keep separation of concerns valid

like image 24
Anton Avatar answered Oct 12 '22 12:10

Anton