Neilski
Neilski

Reputation: 4415

Inserting a transcluded directive into a controller breaks model binding

I am working on my first 'proper' AngularJS project and have encountered a problem when using a transcluded directive within my controller. In overview, I want my transcluded directive to 'wrap' some form elements in my view. Here is the simplified code...

(function () {
   angular.module('testApp', [])
   .directive('xyzFieldSet', function () {
       return {
         template: '<fieldset ng-transclude></fieldset>',
         restrict: 'E',
         transclude: true
       };
   })
   .controller('testCtrl', ['$scope', function($scope) {
       $scope.name = 'Fred';
       $scope.changedName = '';

       $scope.nameChanged = function() {
         $scope.changedName = $scope.name;  
       };
   }]);
}());

and the corresponding HTML...

<div ng-app="testApp">
    <div ng-controller="testCtrl">
        <h2>Without 'fieldset' directive</h2>
        <p>The 'Changed Name' field changes as the 'Name' is changed.</p>
        <p>Name: <input ng-model="name" ng-change="nameChanged()" /></p>
        <p>Changed Name: {{ changedName }}</p>
    </div>    
    <hr />            
    <div ng-controller="testCtrl">
        <h2>With 'fieldset' directive</h2>
        <p>
            With the transcluded directive 'wrapping' the content,
            the 'Changed Name' field <em>does not</em> change as 
            the 'Name' is changed.
        </p>
        <xyz-field-set>
            <p>Name: <input ng-model="name" ng-change="nameChanged()" /></p>
            <p>Changed Name: {{ changedName }}</p>
         </xyz-field-set>
    </div>    
</div>

Without the transcluded directive, any changes to the input field are correctly bound to scope, however, when I add the transcluded directive, the data binding does not work.

A fiddle demonstrating the problem can be found at https://jsfiddle.net/tgspwo73/1/

From what I have read, I am guessing that the directive is changing the scope of its child elements. If this is the case, is there a way of circumventing this behaviour?

Upvotes: 0

Views: 140

Answers (1)

New Dev
New Dev

Reputation: 49590

This has to do with scope prototypical inheritance and the rather unintuitive behavior that results when you don't use a model with a dot (.).

There is a good and exhaustive explanation here:

What are the nuances of scope prototypal / prototypical inheritance in AngularJS?

and my minor contribution here.

The questions/answers in the links above talk about child scopes where the behavior most commonly occurs. For example, ng-if creates a child scope, and so your currently-working approach would break if you did this:

<p>Name: <input ng-if="true"
                ng-model="name" 
                ng-change="nameChanged()" placeholder="Type name here" />
</p>

but similar thing happens with a transcluded scope (with ng-transclude) since this scope prototypically inherits from the parent (although it a child scope of the directive, but that is beside the point).

The way to fix this is to follow the best practice of always binding to a property of an object (i.e. using the . in ng-model):

<xyz-field-set>
  <p>Name: <input ng-model="form.name" 
                  ng-change="nameChanged()" placeholder="Type name here" />
  </p>
  <p>Changed Name: {{ changedName }}</p>
</xyz-field-set>

This necessitates the following changes in the controller:

.controller('testCtrl', ['$scope', function($scope) {
   $scope.form = {
       name: 'Fred' // optionally, set the property
   };

   $scope.changedName = '';

   $scope.nameChanged = function() {
     // this, btw, is unnecessary since you already have $scope.form.name
     // and you can bind to it with {{form.name}}
     // (unless you need to add more logic)
     $scope.changedName = $scope.form.name;  
   };
}]);

Upvotes: 1

Related Questions