Adam Adam - 5 months ago 122
AngularJS Question

Angular two-way data binding isolate scope directive but property is undefined?

Hello I think I don't understand what two-way data binding is. First the code:

.directive('mupStageButtons', function() {
return {
transclude: true,
template: '<span ng-transclude></span>',
replace: true,
scope: {
property: "=",
action: "="
},
controller: function($scope) {
console.log($scope); //I can see the property of $scope defined in console
console.log($scope.property); //undefined
this.property = $scope.property;
this.changeStage = $scope.action; //anyway this is ok
},
};
})
.directive('mupStageButton', function() {
return {
transclude: true,
templateUrl: '/static/templates/directives/StageButton.html',
require: '^^mupStageButtons',
scope: {
value: "=",
btnClass: "@",
},
link: function(scope, element, attrs, mupStageButtonsCtrl, transclude) {
scope.property = mupStageButtonsCtrl.property;
scope.changeStage = mupStageButtonsCtrl.changeStage;
}
};
})

//html

<mup-stage-buttons property="company.stage" action="setStage">
<mup-stage-button value="0" btn-class="btn-default-grey">
</mup-stage-button>
</mup-stage-buttons>


//controller for that html ^^^

.controller('CompanyDetailController', function($scope, $stateParams, Company){
Company.query ({
id : $stateParams.companyId
}, function (data) {
$scope.company = new Company(data);
});
}

//template for <mup-stage-button>

<label ng-class="property === value ? 'active' : 'btn-on-hover' " class="btn {{btnClass}}" ng-click="changeStage(value)">
<div ng-transclude></div>
</label>


Does the "=" mean, that the change in outside scope will propagate thanks to the data binding brag? Or not? Because I fetch a $resource and it is of course defined after the time it is fetched, but the "property" remains undefined. So what is wrong?

EDIT: desired behavior is that the ng-class in the template for
<mup-stage-button>
works

EDIT: plunker: https://plnkr.co/edit/drXxyMpd2IOhXMWFj8LP?p=preview

Answer

You are missing an important thing about the transclude option: the wrapped content is bound to the OUTER scope rather than the directive's scope.

So, here how the scope bindings will look in your case after compilation:

<div ng-controller="CompanyDetailController">
    <mup-stage-buttons property="company.stage" action="setStage"> <-- even though the 'property' is bound correctly, it is not available below due to transclusion -->
        <span ng-transclude>
            {{company.stage}} <!-- CompanyDetailController $scope available here due to transclusion, 'property' is not available! -->

            <mup-stage-button property="company.stage" value="0"> 
                <!-- directive's scope here, binding to the outer scope's 'company.stage' can be used here -->
                {{property}} - {{value}} <!-- this will work -->
                <label ng-class="property === value ? 'active' : 'btn-on-hover' " class="btn {{btnClass}}" ng-click="changeStage(value)">
                    <div ng-transclude>
                        <!-- transcluded content here, bound to the CompanyDetailController $scope -->
                        not working ng-class 0
                    </div>
                </label>
            </mup-stage-button>
        </span>
    </mup-stage-buttons>
</div>

So, to make your code work (Plunk) it would be enough to map the property to the company.stage on the child directive only.

UPDATE

To avoid repetition of the property="company.stage" binding on the child directives and pass the data through the controller and link function of the parent and child directives respectively, you should use the wrapping object for you scope properties, so that you could pass the reference to that object through. Any changes to this object will be available to the child scopes as they will have a reference to that object, this is called the dot notation:

CompanyDetailController:

$scope.vars = {};
this.getCompany = function () {
  $scope.vars.company = $scope.company = {stage: 0}; 
};

then bind the vars property to the parent directive's scope:

// ...
scope: {
    vars: '=',
},
controller: function($scope) {
    this.vars = $scope.vars;
}
// ...

then put the reference of vars to the child directive's scope:

// ...
link: function(scope, element, attrs, mupStageButtonsCtrl, transclude) {
    scope.vars = mupStageButtonsCtrl.vars;
}
// ...

and finally have access to it in the child directive's view:

<label ng-class="vars.company.stage === value ? 'active' : 'btn-on-hover'">...</label>

This way there is no need to repeat the bindings on the child directive instances.

Plunk is updated.

Comments