3

I am trying to bind to a property via ngModel in a layer of directives 3 levels deep. This would be fine, except the middle level contains a ng-if which I believe creates a new scope. The binding is lost at this point.

I have created a jsfiddle to explain the situation: http://jsfiddle.net/5fmck/2/

Note that it works if the ng-if directive is removed, but I am using ng-if instead of ng-show for performance reasons

Does anyone know how I can get the original ngModel to update from the 'inputDirective' template in the fiddle?

Erin Drummond
  • 5,347
  • 6
  • 35
  • 41

2 Answers2

5

Simple :3

Just remember, that child scope is created = use reference to $parent :)

<div ng-if='someCondition'>
    <span>In Wrapper</span>
    <input-directive ng-model='$parent.ngModel'></input-directive>
</div>

http://jsfiddle.net/5fmck/3/

// upd

As I know you need to use reference to $parent only if ngModel is primitive, not object.

Miraage
  • 3,334
  • 3
  • 26
  • 43
  • While this might work, it is a bit brittle with respect to changes in the scope hierarchy. – Michal Charemza Apr 09 '14 at 06:47
  • I like this answer the best because users of my directive can use ngModel with a primitive (which is the primary use-case in my case) and everything works as expected. I have control over the amount of nested scopes, so I know how many $parent's to append to ngModel within the directive. Thanks! – Erin Drummond Apr 09 '14 at 22:00
0

I suspect this is due to the nature of how scopes inherit from each other, and so you should use objects on the scope, and not primitives, and always pass the objects into directives via attributes if it's to be then to be used in another scope. So instead of:

$scope.test = "test";

and

<wrapper-directive ng-model="test" some-condition="true">

use:

$scope.userInput = {
  test: "Test"
}

and

<wrapper-directive user-input="userInput" some-condition="true"> 

Which can be seen at http://jsfiddle.net/4RBaN/1/ (I've also changed all-but-one of the ngModels to another custom attribute, as if you're not using ngModel specific things, like ngModelController, or integration with ngForm, then I think better to KISS).

The reason is that if you have 2 scopes in a parent/child relationship (via prototypical inheritance), such as the one created by ngIf, if you execute (or if Angular executes):

$parentScope.test = 'test';
$childScope.test = 'My new value';

Then $parentScope.test will still equal 'test'. However, if you use objects:

$parentScope.userInput = {test: 'test'}
$childScope.userInput.test = 'My new value';

Then $parentScope.userInput.test will then equal 'My new value'.

The standard quote from (whom I believe was) the original author of Angular, is "If you haven't got a dot in your model, you're doing it wrong". There are other questions on SO about this, such as If you are not using a .(dot) in your AngularJS models you are doing it wrong?

Edit: If you do want to use ng-model, and always pass a primitive to the directive, and not an object, then there is a way that allows this. You'll need to:

  • Always ensure there is a dot in the model that is passed to ngModel, in every directive. Each directive therefore needs to create a new object on its scope in its controller (or linking function). So the template for your 'middle' directive will be:

    <input-directive ng-model='userInput.test'>
    

    And in its controller:

    $scope.userInput = {
      test: $scope.ngModel   
    }
    

    Where ngModel is bound to the value specified in the directive's attributes:

    "scope" : {
      "ngModel" : "="
    }
    
  • Set up manual watchers in each directive, so that changes to the model from the inner directives get propagated back up the chain:

    $scope.$watch('userInput.test', function(newTest) {
      $scope.ngModel = newTest;    
    });
    

This can be seen at: http://jsfiddle.net/zT9sD/ . I have to say, it feels a bit complicated, so I'm not quite sure I can recommend it. Still, (in my opinion) better than using $parent, as introducing a new scope can quite easily happen as things get more complicated, and then you'll have to use things like $parent.$parent.

Community
  • 1
  • 1
Michal Charemza
  • 25,940
  • 14
  • 98
  • 165
  • Like in the other answer, this method requires that the final directive have knowledge of the 'test' property, which is counter-intutive to the usage of ng-model in the first directive. I would like users of the top directive to be able to specify ng-model="someobject.somevalue" (which uses dot notation) and have the final directive in the hierarchy two-way bind to "someobject.somevalue" from the original scope as expected, not "someobject.somevalue.test" – Erin Drummond Apr 09 '14 at 22:08
  • I've added a part that allows the top directive to specify: `ng-model="someobject.somevalue"`, without then having to use `$parent`. I have to admit though, it's more complicated than I would like for what it does. – Michal Charemza Apr 11 '14 at 18:41