1

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 data binding? 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

Adam
  • 1,724
  • 4
  • 21
  • 31
  • It depends on how you're calling it. However, it's likely that you're rendering the directive before the data is available. A good strategy for dealing with this is to place an ng-if on the directive element that only renders once the data is available. – David L Jun 04 '16 at 21:20
  • Ok thanks I'll try. But I thought it would update its value as it usually does. I thought it was just a reference to the memory, so that it would be the same variable and not another not in sync – Adam Jun 04 '16 at 21:23
  • It works, but I need it in sync, actually. Just a reference to the existing variable – Adam Jun 04 '16 at 21:26
  • You'll need a watch in the consuming component then. – David L Jun 04 '16 at 21:26
  • But normally without the directives there's no need for watch. Angular does it itself may I ask why? – Adam Jun 04 '16 at 21:29
  • Should work fine even when parent controller variable is declared in async loading. Need more code context if there is a specific problem – charlietfl Jun 04 '16 at 21:32
  • Mixing `this` and $scope which is confusing especially since it doesn't appear you are using `controllerAs` in views – charlietfl Jun 04 '16 at 22:42
  • Also important to understand that isolated scope is only valid inside template. create a demo that replicates issue – charlietfl Jun 04 '16 at 22:48
  • @charlietfl Added plunker https://plnkr.co/edit/drXxyMpd2IOhXMWFj8LP?p=preview – Adam Jun 05 '16 at 11:07
  • What should I be seeing. Only see "this works" or "not works" but no explanation of expected results or what interaction is required. Try adding comments and saving again in demo – charlietfl Jun 05 '16 at 11:11
  • @charlietfl done (description in html) – Adam Jun 05 '16 at 11:23

2 Answers2

2

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.

Adam
  • 1,724
  • 4
  • 21
  • 31
Alexander Kravets
  • 4,245
  • 1
  • 16
  • 15
  • I mean the ng-class on the label based on `property` is not working. I pass the `property` to the child directive's scope in link function via the parent's controller. – Adam Jun 05 '16 at 16:27
  • So that more child buttons can share one property accessed via parent's controller – Adam Jun 05 '16 at 16:28
  • @Adam, does the [plunk](https://plnkr.co/edit/HM92L1rxLs4vuHGVu4DX?p=preview) work as expected? The way you try to do it via controllers will not work, you have to use the bindings. – Alexander Kravets Jun 05 '16 at 16:44
  • It does work as expected, but I'd rather bind the property once in a parent directive, than for every child button. – Adam Jun 05 '16 at 17:43
  • @Adam, I updated the answer with the workaround to avoid the binding repetition in child directive instances. – Alexander Kravets Jun 06 '16 at 06:03
  • Thanks, but I've solved it with $scope.$watch in the parent directive's controller. The workaround is not good for me, because I don't always know that the property will be named "stage". I'm planning to use the directive in more cases than this one with company."stage". However rather than the $watch I'd like a vanilla js solution, but perfomace-wise it is not a huge deal, so I'd stick with it. – Adam Jun 06 '16 at 15:36
0

In javascript

Primitives are passed by value, Objects are passed by "copy of a reference".

Solution using $watch:

.directive('mupStageButtons', function() {
    return {
        transclude: true,
        template: '<span ng-transclude></span>',
        replace: true,
        scope: {
            property: "=",
            action: "="
        },
        controller: function($scope) {
            that = this;
            $scope.$watch('property', function(newValue){
                that.property = newValue;    
      /***Refresh this.property (normal assignment would only copy value, 
     it would not behave as a reference to desired transcluded property)***/
            });
            this.changeStage = $scope.action;
        },
    };
})
.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.btnCtrl = mupStageButtonsCtrl;
            scope.changeStage = mupStageButtonsCtrl.changeStage;
        }
    };
})

An important part besided the $watch is also this in link function:

scope.btnCtrl = mupStageButtonsCtrl;

We could not do

scope.property = mupStageButtonsCtrl.property;

because it would just copy the value, and when it changed in the ctrl, it wouldn't change here in the child directive. So we assign ctrl reference to scope.btnCtrl and it works. Template for child directive:

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

Now I can use the directives generically as I need - pass just the property like company.stage, so that the directive doesn't need to know the property name (stage).

<mup-stage-buttons property="company.stage" action="setStage">
    <mup-stage-button value="0" btn-class="btn-default-grey">
        Stage 0
    </mup-stage-button>
</mup-stage-buttons>
Community
  • 1
  • 1
Adam
  • 1,724
  • 4
  • 21
  • 31