berno berno - 1 month ago 8
Javascript Question

AngularJS binding a javascript object to a directive attribute

i have a problem when I try to bind a javascript object in an attribute of a directive.

I have a controller in which I make an $http.get request to a rest api service, i receive some data among which a JSON string representing an itinerary for a map canvas.

.controller('myController', ['$scope', '$http', function($scope,$http) {
$http.get(endpointUrl)
.success(function(data){
if(data.status == 'ok'){
$scope.itinerary = angular.fromJson(data.itinerary);
}
});
}]);


If I log $scope.itinerary in console I see it's a valid javascript object

{"origin":"Piazza Adua, Firenze","destination":"Galleria degli Uffizi, Firenze","waypoints":[{"location":"Fiesole, Firenze","stopover":true},{"location":"Piazza Santa Croce, Firenze","stopover":true}],"optimizeWaypoints":false,"travelMode":"DRIVING"}


I need to bind that object in a directive attribute.

.directive('map', function(){
return {
restrict: 'E',
scope: {
myItinerary: "="
},
templateUrl: 'templates/directive/map.html',
link: function(scope,el,attrs) {
console.log(attrs);
}
}
});


This is the HTML

<map my-itinerary="itinerary"></map>


When I log attrs inside the directive link function the attribute myItinerary is the string "itinerary" and not the object like i would expect.
I tried to interpolate itinerary with {{ }} but , as I would expect, this generate a Syntax Error.
I also tried to switch '=' with '@' and bind the json string instead of the javascript object but the value of attrs.myItinerary inside the directive is an empty string.
I saw many examples to bind an object to a directive and no one has a problem with the same approach i used.
Since i started using angular recently I would like to know if my approach is wrong or why it does not work.
Thanks in advance for help.

Answer

Original Issue

When you bind something to a directive that bound value is available on the $scope object. Not the attrs object.

So in the controller $scope.controllerParam = 10

and implemented in the view <my-directive input="controllerParam"></my-directive>

You could have a directive markup like so

.directive('myDirective', function () {
    return {
        restrict: E,
        scope: {
            input: '='
        },
        link: function ($scope, e, attrs) {
            console.log($scope.input); //Will log 10
            console.log(attrs.input); //Will log string controllerParam
        }
    }
});

Updating content in your Link

This is from your comments about data sourced from an API.

Your link function is run once when your directive first runs. That is why when you console.log your scope the value myItinerary is undefined as the AJAX call has not returned at that point. If you were to put the console.log in a timeout it would work as the AJAX request has returned and value updated through data-binding at that point. However that is a terrible idea for dealing with dynamic content like that.

Watch

If you need to perform some logic of a bound variable in your link function you can put a watch on it so that if it is changed inside or outside your directive your watch handler will fire.

$scope.$watch('myItinerary', function(newValue, oldValue) {
  console.log(newValue, oldValue);
});

However if your adding lots of these they can come at a performance hit as every watch is assessed every time a digest cycle is run, pluss handling that fact they can be triggered from changes inside or outside your directive can be tough to deal with.

Services

Generally speaking all your ajax requests should be defined in a service and injected into what ever needs them. So if a directive has a single purpose it is feasible that it could make that request itself by injecting the service directly into it.

Binding a function Your directive could potentially also expose a function that your controller could bind to. When your controller has data it could call this function passing it data.

//View
<my-directive update="update"></my-directive>

//in controller simply do
$scope.update('new data');

//directive

.directive('myDirective', function () {
    return {
        restrict: E,
        scope: {
            update: '='
        },
        link: function ($scope, e, attrs) {
            $scope.update = function (d) {
                console.log(d);
            }
        }
    }
});

You have to be careful though if you try to call a bound function that hasn't been updated yet or if the directive has it as optional. In either case the bound variable in the controller would be undefined.

Out of them all in my opinion the service is best. Falling back to watches when a service is not suitable. I avoid exposing functions as it just feels wrong to me but its possible. To be fair i very rarely have a need to use watches in the stuff ive had to do.

Sorry for the essay