11

I prepared a little fiddle and boiled it down to the minimum:

http://jsfiddle.net/lpeterse/NdhjD/4/

<script type="text/javascript">
    angular.module('app', ['ui.bootstrap']);

    function Ctrl($scope) {
      $scope.foo = "42";
}
</script>


<div ng-app="app" ng-controller="Ctrl">
    1: {{foo}}<br />
    2: <input ng-model="foo" />
    <tabs>
        <pane heading="tab">
            3: {{foo}}<br />
            4: <input ng-model="foo" />
        </pane>
    </tabs>    
</div>

In the beginning all views reference the model Ctrl.foo.

If you change something in input 2: it properly updates the model and this change gets propagated to all views.

Changing something in input 4: only affects the views included in the same pane. It behaves like the scope somehow forked. Afterwards changes from 2: don't get reflected in the tab anymore.

I read the angular docs on directives, scopes and transclusion, but couldn't find an explanation for this undesired behaviour.

I would be grateful for any hints :-)

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
Lars Petersen
  • 113
  • 1
  • 6

3 Answers3

12

The problem is the same as in ng-repeat when you edit a primitive - the <pane> directive creates a new scope which inherits from the parent.

Now, given the way Javascript inheritance works the <pane> directive has its own copy of the foo string primitive, and when you edit it you are only editing it on the pane child scope.

A simple solution would be to put foo in an object on your parent Ctrl:

function Ctrl($scope) {
  $scope.data = { foo: 42 };
}

Then you can do this in your HTML:

<tabs><pane><input ng-model="data.foo"></pane></tabs>

Why does it work with an object? Because when <pane> inherits the parent's scope, its reference to data will refer to the same object in memory as on the parent Ctrl. Primitives like strings and numbers are copied in inheritance, and objects simply create a new pointer to the same object.

TL;DR: <pane>'s new scope inherits the foo string primitive as a new copy of foo which when edited won't change on the parent Ctrl. <pane>'s new scope would inherit an object like data as a reference to the same object, and when edited on the <pane> scope the same object would be referenced on the parent scope.

Helpful article: https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance

Andrew Joslin
  • 43,033
  • 21
  • 100
  • 75
  • Okay, as you explain it makes kind of sense. I hadn't expected that a new scope literally copies the elements from the scope it is derived from which of course leads to this behaviour for primitives. Still, very confusing that that new scope is created on first write. Before that the views still reflect directly the original scope. tl;dr: don't use primitives :-) – Lars Petersen Feb 19 '13 at 16:28
  • @Lars, primitives are not copied when the child scope is created. See my answer for a little more clarification on when/how the parent scope primitive values get "copied" to the child scope. – Mark Rajcok Feb 28 '13 at 17:30
  • This sorted it for me! – Pete Jan 02 '14 at 12:00
4

The <tabs> and <pane> directives each create a new transcluded child scope (because they both have transclude: true,) which prototypically inherits from the parent scope, and an isolate child scope which does not prototypically inherit from the parent scope. The <input...> inside the <pane> uses the transcluded child scope.

When the input inside the <pane> is first rendered, it is populated with the value of $scope.foo. Normal JavaScript prototypal inheritance comes into play here... initially foo is not defined on the transcluded child scope (prototypal inheritance does not copy primitives), so JavaScript follows the prototype chain and looks at the parent object/$scope, and finds it there. 42 is put into the textbox. The transcluded child scope is not affected/changed (yet).

If you edit the first textbox, the second textbox is updated because JavaScript is still using prototypal inheritance to find the value of $scope.foo.

If you edit the second textbox, to say 429, Angular writes the value to $scope.foo, but note that $scope is the transcluded child scope. Since foo is a primitive, it creates a new property on that child scope -- that's how JavaScript works, for better or worse. This new property will shadow/hide the parent scope property of the same name. Prototypal inheritance is not in play here. (The article Andy mentions in his post (which is also on SO) also explains this in detail, with pictures.) Since the transcluded child scope now has a foo property, it will now use that local property for reading and writing, so it appears "disconnected" from the parent scope.

Using an object (rather than a primitive) solves the problem because prototypal inheritance is then always in play. The transcluded child scope gets a reference to the object in the parent scope. Writing to data.foo writes to the data object on the parent, not the transcluded child scope.

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
0

The problem lies in the tabs directive. I think on line 1044 of ui-bootstrap-tpls-0.1.0.js.

If you change scope: {} to scope: { foo: '='} it should give you a two-way data binding.

From Angular Docs:

= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value of the attr attribute. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localModel:'=myAttr' }, then widget scope property localModel will reflect the value of parentModel on the parent scope. Any changes to parentModel will be reflected in localModel and any changes in localModel will reflect in parentModel.

Andrew Joslin
  • 43,033
  • 21
  • 100
  • 75
mb21
  • 34,845
  • 8
  • 116
  • 142