Angular.js: ng-if breaking two-way binding

Created on 18 Sep 2013  路  6Comments  路  Source: angular/angular.js

I'm using 1.2 RC2 and I found an issue with ng-if.

If I put a 2-way binding inside of an ng-if directive, the binding breaks as soon as you start writing into the input. I have recreated it here:

http://codepen.io/BrianGenisio/pen/hEkun

If you change ng-if to ng-show, everything works.

Most helpful comment

@facboy to paraphrase Mi拧ko, "If you don't have a dot in your model, you're doing it wrong".

JSObjects are reference counted, so if you put your binding variable in an object, you can share the object with child scopes, and the variable can be accessed from the correct context.

There's been some talk about making this more transparent for angular 2.0, but to be honest it would probably cause even more problems that way.

2-way binding will always work, so long as you ensure that both scopes are using the correct context. It's not very hard to do.

This will break because you write to fooModel in the child scope, but you want to write it in the parent scope:

<div ng-controller="FooCtrl">
  <div ng-if="someValue">
    <input ng-model="fooModel" name="fooModel">
  </div>
  <tt>{{fooModel}}</tt>
</div>

This will work because you're always using the same context for both the parent and child scope:

<div ng-init="model = {}" ng-controller="FooCtrl">
  <div ng-if="someValue">
    <input ng-model="model.foo" name="fooModel">
  </div>
  <tt>{{model.foo}}</tt>
</div>

Also note, this is entirely up to the caller of the directive, it's not the directive's responsibility to worry about this, unless it creates its own child scopes.

It's totally intuitive once you understand what's happening

All 6 comments

This is not an issue with ng-if. It happens for any directive that creates a new child scope. See http://codepen.io/anon/pen/AhxLD for an example.
This is because the ng-model writes its model value to the child scope, which then hides the outer scope.
This document will explain more: https://github.com/angular/angular.js/wiki/Understanding-Scopes

Wow! Ok. I would have never expected ng-if to create a child scope. My mental model had it being identical to ng-show but it removes the element instead of hiding it. For my own edification, why is this desirable?

I ran into this with using forms. Forms assign themselves to the scope using normal attribute assignment (ie scope[formName] = form) instead of an angular setter, so if you have a form wrapped in an ng-if, you can't easily get access to the form, and you can't assign it to a nested model. It is not clear or intuitive to a user that ng-if would create a child scope.

Edit: looks like the form bug is fixed in 1.2. Though I still agree it isn't obvious that ng-if would create a child scope.

I think this is very unintuitive behaviour...it makes two-way binding mostly useless unless you are very sure about the context in which a directive is going to be used. It also means that if a developer inadvertently introduces eg an ng-if into a template that they potentially break the behaviour of any directives contained within that ng-if.

So really, it's '2-way binding if the directive is directly in your controller scope and will always be there'. Otherwise it's '1-way binding until the directive tries to modify the value, then no-binding at all'.

@facboy to paraphrase Mi拧ko, "If you don't have a dot in your model, you're doing it wrong".

JSObjects are reference counted, so if you put your binding variable in an object, you can share the object with child scopes, and the variable can be accessed from the correct context.

There's been some talk about making this more transparent for angular 2.0, but to be honest it would probably cause even more problems that way.

2-way binding will always work, so long as you ensure that both scopes are using the correct context. It's not very hard to do.

This will break because you write to fooModel in the child scope, but you want to write it in the parent scope:

<div ng-controller="FooCtrl">
  <div ng-if="someValue">
    <input ng-model="fooModel" name="fooModel">
  </div>
  <tt>{{fooModel}}</tt>
</div>

This will work because you're always using the same context for both the parent and child scope:

<div ng-init="model = {}" ng-controller="FooCtrl">
  <div ng-if="someValue">
    <input ng-model="model.foo" name="fooModel">
  </div>
  <tt>{{model.foo}}</tt>
</div>

Also note, this is entirely up to the caller of the directive, it's not the directive's responsibility to worry about this, unless it creates its own child scopes.

It's totally intuitive once you understand what's happening

@caitp I see. Thanks!

Was this page helpful?
0 / 5 - 0 ratings