Two way binding not working in directive with transcluded scope

JavascriptAngularjs

Javascript Problem Overview


I've a textbox in a controller which is bound to model name. There's a directive inside the controller and there's another textbox inside the directive which is bound to the same model name:

<div class="border" ng-controller="editCtrl">
   Controller: editCtrl <br/>
   <input type="text" ng-model="name" />
   <br/>
   <tabs>
      Directive: tabs <br/>
      <input type="text" ng-model="name"/>
   </tabs>
</div>

mod.directive('tabs', function() {
  return {
    restrict: 'E',
    transclude: true, 
    template:
      '<div class="border" ng-transclude></div>',
  };
});

When you type something in the outer textbox it's reflected in the inner textbox but if you type something in the inner textbox it stops working i.e. both textbox no more reflects the same value.

See example at: http://jsfiddle.net/uzairfarooq/MNBLd/

I've also tried using two way binding attr (scope: {name: '='}) but it gives syntax error.And using scope: {name: '@'} has same effect.

Any help would be greatly appreciated.

In addition to the accepted answer, this article really helped me in understanding the prototypical inheritance in child scpoes. I'd highly recommend anyone having problem with scopes to read it thoroughly.

Javascript Solutions


Solution 1 - Javascript

A directive with transclude: true results in the directive creating a new (transcluded) child scope. This new scope prototypically inherits from the parent scope. In your case, the parent scope is the scope associated with the editCtrl controller.

Using two-way databinding in a child scope (i.e., ng-model) to bind to a parent scope property that holds a primitive value (e.g., name) always causes problems -- well, I should say that it doesn't work as expected. When the scope property is changed in the child (e.g., you type into the second textbox) the child creates a new scope property that hides/shadows the parent scope property of the same name. If the parent property holds a primitive value, that value is (essentially) copied to the child property when the child property is created. Future changes in the child scope (e.g., the second textbox) only affect the child property.

Before typing into the second textbox (i.e., before the property is changed in the child), the child/transcluded scope finds the name property in the parent scope via prototypal inheritance (dashed line in picture below). This is why the two textboxes initially remain in synch. Below, if you type "Mark" into the first text box, this is what the scopes look like:

transcluded scope follows inheritance chain

I created a fiddle where you can examine the two scopes. Click the "show scope" link next to the second textbox before typing into the second textbox. This will allow you to see the transcluded child scope. You will notice that it does not have a name property at this point. Clear the console, type into the second text box, then click the link again. You will notice that the child scope now has a name property, and the initial value was the value that parent property had ("Mark"). If you typed " likes Angular" into the second text box, this is what the scopes look like:

transcluded primitive hides parent property

There are two solutions:

  1. do what @pgreen2 suggests (this is the "best practice" solution) -- use an object instead of a primitive. When an object is used, the child/transcluded scope does not get a new property. Only prototypal inheritance is in play here. In the picture below, assume the editCtrl's $scope has this object defined:
    $scope.myObject = { name: "Mark", anotherProp: ... }:
    object in parent
  2. use $parent in the child scope (this is a fragile solution, and not recommended, as it makes assumptions about HTML structure): use ng-model="$parent.name" inside the <input> that is within the <tabs> element. The first picture above shows how this works.

A syntax error occurs when using scope: {name: '='} because when using two-way databinding (i.e., when using '='), interpolation is not allowed -- i.e., {{}} can't be used. Instead of <tabs name="{{name}}"> use <tabs name="name">.

Using '@' works the same as the transclude case because ng-transclude uses the transcluded scope, not the isolate scope that is created by using scope: { ... }.

For (lots) more information about scopes (including pictures) see
https://stackoverflow.com/questions/14049480/what-are-the-nuances-of-scope-prototypal-prototypical-inheritance-in-angularjs/14049482

Solution 2 - Javascript

I believe that the problem has to do with scoping. Initially the inner textbox doesn't have name set, so it is inherited from the outer scope. This is why typing in the outer box is reflected in the inner box. However, once typing in the inner box occurs, the inner scope now contains name which means it is no longer bound to the outer name so the outer text box doesn't sync up.

The appropriate way to fix is only storing models in the scope, not your values. I fixed it in http://jsfiddle.net/pdgreen/5RVza/ The trick is to create a model object (data) and referencing values on it.

The incorrect code modifies the scope in the directive, the correct code modifies the model in the scope in the directive. This subtle difference allows the scope inheritance to work properly.

I believe the way Miško Hevery phrased it was, scope should be write-only in the controller, and read-only in directives.

update: reference: https://www.youtube.com/watch?v=ZhfUv0spHCY#t=29m19s

Solution 3 - Javascript

Syntax error means that you miswrote something. It is not related to a particular framework / library. You probably forgot to add "," or close a paranthesis. Check it out again

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionUzair FarooqView Question on Stackoverflow
Solution 1 - JavascriptMark RajcokView Answer on Stackoverflow
Solution 2 - Javascriptpgreen2View Answer on Stackoverflow
Solution 3 - JavascriptUmur KontacıView Answer on Stackoverflow