Thursday, October 30, 2014

AngularJS: Confusing and non-symmetrical callback when using expression binding

There are three ways to communicate values between the directive and the controller


  • For one way string binding, we can use the '@' character
  • For more binding flexibility, we can use bi-directional binding by using the '=' character
  • And then for expression binding, we can use the '&' character



There are two ways to do a callback on Angular, one is to use bi-directional binding, another way is to use expression binding. Expression binding is the recommended way to do a callback, however I find it confusing on some scenario



Here's the complete code:
<div ng-controller="TheController">
  
    <sample-directive the-callback="informParent(theParam)">
        Nice
    </sample-directive>
    
    Received Value: {{valueReceivedFromDirective}}
    
</div>


var theApp = angular.module('theApp',[]);


theApp.directive('sampleDirective', function() {
    return {
        restrict: 'E',
        transclude: true,
        scope : {
            theCallback : '&'
        },
        template: 
        '<div>' + 
            '<div ng-transclude></div>' + 
            '<hr/>' +
            '<div>Comments: <input ng-model="comments" ng-init="comments=\'Great\'"/></div>' +            
            '<div><button ng-click="theCallback({theParam: comments})">Click</button></div>' + 
        '</div>'
    };
});


theApp.controller('TheController', ['$scope', function($scope) {

    $scope.valueReceivedFromDirective = '';
    
    
    // If in case we need to reference a controller's property with same name as directive's parameter 
    // in the callback, there's no way to reference that controller's property. 
    // That is, this can't be referenced in the-callback
    $scope.theParam = 'Blah'; 
    
    // The name must be different from the directive's parameter name:
    $scope.theParamX = 'Meh'; // this can now be referenced in the directive's callback
    
    
    $scope.informParent = function(valuePassed) {    
        // code below is symmetrical to the directive's callback. however, it won't work
        // $scope.valueFromDirective = valuePassed.theParam; 
        
        // so should use this:
        $scope.valueReceivedFromDirective = valuePassed;
    };
    
}]);


Live code: http://jsfiddle.net/jrxh3w1p/1/


As we can see on line 16, we passed an object to the callback parameter, however when it's received on parent callback (line 38), the value received is not the whole object, the parent callback only receives the object's property, hence there's no need to access it as valuePassed.theParam (line 41). The symmetry is broken here

Another oddity, on line 16 it's confusing what property the theParam name is referring to, is it from the directive, or is it from the controller? It could be from the controller, as long as the name doesn't conflict from the directive's parameter name; in our example above, informParent's theParam name is referring to directive instead since the directive has theParam name in its parameter theCallback({theParam: comments}). Try to change line 3 of the HTML, to informParent(theParamX), check it here: http://jsfiddle.net/k4qtL7ez/2/. With expression binding (line 3 of HTML), we cannot ascertain on which property is the parameter in the callback is referring to unless we see the code of the directive; callback by means of expression binding makes the code confusing to read


Another oddity is we cannot pass the comments directly, see: http://jsfiddle.net/tq7pLj8x/1/. Rule is, when using expression binding (uses '&'), the parameter(s) from the directive passed to to the parent's callback must be enclosed in an object. And in parent's callback, the value received is the object's property, not the object itself




If we want to make things more symmetrical, we can use the bi-directional binding by using the '=' sign. Here are the changes:

<div ng-controller="TheController">
  
    <sample-directive the-callback="informParent">
        Nice
    </sample-directive>
    
    Received Value: {{valueReceivedFromDirective}}
    
</div>


var theApp = angular.module('theApp',[]);


theApp.directive('sampleDirective', function() {
    return {
        restrict: 'E',
        transclude: true,
        scope : {
            theCallback : '='
        },
        template: 
        '<div>' + 
            '<div ng-transclude></div>' + 
            '<hr/>' +
            '<div>Comments: <input ng-model="comments" ng-init="comments=\'Great\'"/></div>' +            
            '<div><button ng-click="theCallback({theParam: comments})">Click</button></div>' + 
        '</div>'
    };
});


theApp.controller('TheController', ['$scope', function($scope) {
    $scope.name = 'Superhero';
    
    $scope.valueReceivedFromDirective = '';
    
    $scope.informParent = function(valuePassed) {    
    
        // the parameter passed to this function is now symmetrical how the directive call this function.
        // The directive passed an object to this function, this function received an object too:
        $scope.valueReceivedFromDirective = valuePassed.theParam;
    };
    
}]);

Live code: http://jsfiddle.net/vhx4huk2/


The changes in HTML (line 3) is we didn't call the function directly, we just passed the reference of the controller's method to the directive's property binding (the '=' sign, line #9). With property binding, if we passed an object from the directive's callback (line 16), the parent callback receives an object too (line 31); so if we passed an scalar value from the directive, the parent callback receives scalar value too. See: http://jsfiddle.net/vhx4huk2/1/



If there are no cons against callback using the property binding approach (using '='), it's a lot better than expression binding (using '&'). With property binding approach, there will be no confusion on which property the callback (we only pass the object's method reference) since there is no parameter being passed to it (line 3 of HTML). Another advantage is the caller and callee relationship is very symmetrical, caller passed an object or scalar, the callee receives the same parameter format



Happy Coding!

No comments:

Post a Comment