29

Angular newbie here. I am trying to figure out what's going wrong while passing objects to directives.

here's my directive:

app.directive('walkmap', function() { 
  return {
    restrict: 'A',
    transclude: true,
    scope: { walks: '=walkmap' },
    template: '<div id="map_canvas"></div>',
    link: function(scope, element, attrs)
    {
      console.log(scope);
      console.log(scope.walks);
    }
  };
});

and this is the template where I call the directive:

<div walkmap="store.walks"></div>

store.walks is an array of objects.

When I run this, scope.walks logs as undefined while scope logs fine as an Scope and even has a walks child with all the data that I am looking for.

I am not sure what I am doing wrong here because this exact method has worked previously for me.

EDIT:

I've created a plunker with all the required code: http://plnkr.co/edit/uJCxrG

As you can see the {{walks}} is available in the scope but I need to access it in the link function where it is still logging as undefined.

winkerVSbecks
  • 1,173
  • 1
  • 10
  • 24
  • Can you post plunker/jsfiddle example where your code does not work. I've create plunker with absolutelly same code as in your question and it works: http://plnkr.co/edit/TKVVDXp0m9jnVSRYBxVp?p=preview May be problem with some other part or your code? – Valentyn Shybanov Jan 31 '13 at 06:53
  • 1
    what the the context of store.walks, have u defined store.walks in parent scope if not you have to define store.walks in either local scope or parent scope – Ajay Beniwal Jan 31 '13 at 07:03

5 Answers5

40

Since you are using $resource to obtain your data, the directive's link function is running before the data is available (because the results from $resource are asynchronous), so the first time in the link function scope.walks will be empty/undefined. Since your directive template contains {{}}s, Angular sets up a $watch on walks, so when the $resource populates the data, the $watch triggers and the display updates. This also explains why you see the walks data in the console -- by the time you click the link to expand the scope, the data is populated.

To solve your issue, in your link function $watch to know when the data is available:

scope.$watch('walks', function(walks) {
   console.log(scope.walks, walks);
})

In your production code, just guard against it being undefined:

scope.$watch('walks', function(walks) {
  if(walks) { ... }
})

Update: If you are using a version of Angular where $resource supports promises, see also @sawe's answer.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • That most certainly works. Thanks so much for explaining the asynchronous nature of $resource. I had a feeling that might be the issue, but because `{{walks}}` kept rendering in the template that through me off. – winkerVSbecks Jan 31 '13 at 22:26
  • there is a parenthesis/bracket that should be deleted :) `scope.$watch('walks'), function(walks) {` to `scope.$watch('walks', function(walks) {` – piggyback Feb 07 '13 at 18:53
  • 2
    Updated / complete Plunker with Mark Rajcok's answer here: http://plnkr.co/edit/Ed2Wqv?p=preview. Note that you need to open your browser's (eg. Firefox Firebug or Chrome Dev Tools) console in order to see the console.log(scope.walks); output and thus 'witness' the completed working directive populated asynchronously from the resource data - top work - thanks a million Mark! – Matty J May 30 '13 at 14:57
  • Note that `$resource` depends on `$http` which call asynchronously the success and error functions. So if you use the lower level `$http` you are just the same situation and will need the aid of `$watch`. – Watchmaker Jun 09 '16 at 15:52
12

you may also use

scope.walks.$promise.then(function(walks) {
    if(walks) {
      console.log(walks);
    }
  });
sawe
  • 1,141
  • 14
  • 24
  • This is good because it will wait for the async call to resolve before updating the directive scope – grant Mar 30 '15 at 21:24
3

Another solution would be to add ControllerAs to the directive by which you can access the directive's variables.

app.directive('walkmap', function() { 
  return {
    restrict: 'A',
    transclude: true,
    controllerAs: 'dir',
    scope: { walks: '=walkmap' },
    template: '<div id="map_canvas"></div>',
    link: function(scope, element, attrs)
    {
      console.log(scope);
      console.log(scope.walks);
    }
  };
});

And then, in your view, pass the variable using the controllerAs variable.

<div walkmap="store.walks" ng-init="dir.store.walks"></div>
Linus
  • 4,643
  • 8
  • 49
  • 74
2

Try:

<div walk-map="{{store.walks}}"></div>

angular.module('app').directive('walkMap', function($parse) {
  return {
    link: function(scope, el, attrs) {
      console.log($parse(attrs.walkMap)(scope));
    }
  }
});
Elise Chant
  • 5,048
  • 3
  • 29
  • 36
  • This worked for me, not sure what the (scope) does, was able to get it to work with $parse(attrs.walkMap)() – TOBlender Sep 17 '15 at 22:42
0

your declared $scope.store is not visible from the controller..you declare it inside a function..so it's only visible in the scope of that function, you need declare this outside:

app.controller('MainCtrl', function($scope, $resource, ClientData) {
  $scope.store=[];         // <- declared in the "javascript" controller scope
  ClientData.get({}, function(clientData) {
  self.original = clientData;
  $scope.clientData = new ClientData(self.original);
  var storeToGet = "150-001 KT";
  angular.forEach(clientData.stores, function(store){
   if(store.name == storeToGet ) {
     $scope.store = store;      //declared here it's only visible inside the forEach
     }
   });
  });
 });
clagccs
  • 2,224
  • 2
  • 21
  • 33
  • Just tried that and it makes no difference. The `{{walks}}` is available in the directive template, that's why you can see all the data, but it's not available in the link function. – winkerVSbecks Jan 31 '13 at 18:20