Reputation: 4415
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
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