Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

angular-bootstrap (tabs): data binding works only one-way

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 :-)

like image 845
Lars Petersen Avatar asked Feb 19 '13 15:02

Lars Petersen


2 Answers

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

like image 75
Andrew Joslin Avatar answered Sep 30 '22 05:09

Andrew Joslin


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.

like image 35
Mark Rajcok Avatar answered Sep 30 '22 05:09

Mark Rajcok