In Angular, how to pass JSON object/array into directive?

101,271

Solution 1

If you want to follow all the "best practices," there's a few things I'd recommend, some of which are touched on in other answers and comments to this question.


First, while it doesn't have too much of an affect on the specific question you asked, you did mention efficiency, and the best way to handle shared data in your application is to factor it out into a service.

I would personally recommend embracing AngularJS's promise system, which will make your asynchronous services more composable compared to raw callbacks. Luckily, Angular's $http service already uses them under the hood. Here's a service that will return a promise that resolves to the data from the JSON file; calling the service more than once will not cause a second HTTP request.

app.factory('locations', function($http) {
  var promise = null;

  return function() {
    if (promise) {
      // If we've already asked for this data once,
      // return the promise that already exists.
      return promise;
    } else {
      promise = $http.get('locations/locations.json');
      return promise;
    }
  };
});

As far as getting the data into your directive, it's important to remember that directives are designed to abstract generic DOM manipulation; you should not inject them with application-specific services. In this case, it would be tempting to simply inject the locations service into the directive, but this couples the directive to that service.

A brief aside on code modularity: a directive’s functions should almost never be responsible for getting or formatting their own data. There’s nothing to stop you from using the $http service from within a directive, but this is almost always the wrong thing to do. Writing a controller to use $http is the right way to do it. A directive already touches a DOM element, which is a very complex object and is difficult to stub out for testing. Adding network I/O to the mix makes your code that much more difficult to understand and that much more difficult to test. In addition, network I/O locks in the way that your directive will get its data – maybe in some other place you’ll want to have this directive receive data from a socket or take in preloaded data. Your directive should either take data in as an attribute through scope.$eval and/or have a controller to handle acquiring and storing the data.

- The 80/20 Guide to Writing AngularJS Directives

In this specific case, you should place the appropriate data on your controller's scope and share it with the directive via an attribute.

app.controller('SomeController', function($scope, locations) {
  locations().success(function(data) {
    $scope.locations = data;
  });
});
<ul class="list">
   <li ng-repeat="location in locations">
      <a href="#">{{location.id}}. {{location.name}}</a>
   </li>
</ul>
<map locations='locations'></map>
app.directive('map', function() {
  return {
    restrict: 'E',
    replace: true,
    template: '<div></div>',
    scope: {
      // creates a scope variable in your directive
      // called `locations` bound to whatever was passed
      // in via the `locations` attribute in the DOM
      locations: '=locations'
    },
    link: function(scope, element, attrs) {
      scope.$watch('locations', function(locations) {
        angular.forEach(locations, function(location, key) {
          // do something
        });
      });
    }
  };
});

In this way, the map directive can be used with any set of location data--the directive is not hard-coded to use a specific set of data, and simply linking the directive by including it in the DOM will not fire off random HTTP requests.

Solution 2

As you say, you don't need to request the file twice. Pass it from your controller to your directive. Assuming you use the directive inside the scope of the controller:

.controller('MyController', ['$scope', '$http', function($scope, $http) {
  $http.get('locations/locations.json').success(function(data) {
      $scope.locations = data;
  });
}

Then in your HTML (where you call upon the directive).
Note: locations is a reference to your controllers $scope.locations.

<div my-directive location-data="locations"></div>

And finally in your directive

...
scope: {
  locationData: '=locationData'
},
controller: ['$scope', function($scope){
  // And here you can access your data
  $scope.locationData
}]
...

This is just an outline to point you in the right direction, so it's incomplete and not tested.

Solution 3

What you need is properly a service:

.factory('DataLayer', ['$http',

    function($http) {

        var factory = {};
        var locations;

        factory.getLocations = function(success) {
            if(locations){
                success(locations);
                return;
            }
            $http.get('locations/locations.json').success(function(data) {
                locations = data;
                success(locations);
            });
        };

        return factory;
    }
]);

The locations would be cached in the service which worked as singleton model. This is the right way to fetch data.

Use this service DataLayer in your controller and directive is ok as following:

appControllers.controller('dummyCtrl', function ($scope, DataLayer) {
    DataLayer.getLocations(function(data){
        $scope.locations = data;
    });
});

.directive('map', function(DataLayer) {
    return {
        restrict: 'E',
        replace: true,
        template: '<div></div>',
        link: function(scope, element, attrs) {

            DataLayer.getLocations(function(data) {
                angular.forEach(data, function(location, key){
                    //do something
                });
            });
        }
    };
});
Share:
101,271
Omegalen
Author by

Omegalen

Updated on July 20, 2022

Comments

  • Omegalen
    Omegalen almost 2 years

    Currently, my app has a controller that takes in a JSON file then iterates through them using "ng-repeat". This is all working great, but I also have a directive that needs to iterate through the same JSON file. This is posing a problem as I cannot request the same JSON file twice on one page (nor would I want to because it would be inefficient). Both the directive and controller request and iterate through the JSON data just fine if I change the filename of one of the JSON files.

    What I'm wondering is: what's the best way to go about passing the array formed from my controller's JSON request into the directive? How can I pass the array into my directive and iterate through when I've already accessed it via my controller?

    Controller

    appControllers.controller('dummyCtrl', function ($scope, $http) {
       $http.get('locations/locations.json').success(function(data) {
          $scope.locations = data;
       });
    });
    

    HTML

    <ul class="list">
       <li ng-repeat="location in locations">
          <a href="#">{{location.id}}. {{location.name}}</a>
       </li>
    </ul>
    <map></map> //executes a js library
    

    Directive (Works when I use a file name besides locations.json, since I've already requested it once

    .directive('map', function($http) {
       return {
         restrict: 'E',
         replace: true,
         template: '<div></div>',
         link: function(scope, element, attrs) {
    
    $http.get('locations/locations.json').success(function(data) {
       angular.forEach(data.locations, function(location, key){
         //do something
       });
    });
    
  • Rodrigo Fonseca
    Rodrigo Fonseca over 10 years
    using a controller to reuse and get data from server? do you seriously think that this is a job for the controller?
  • Omegalen
    Omegalen over 10 years
    I use my controller separately from my directive. Can I still accomplish what I want through your recommendation?
  • Rodrigo Fonseca
    Rodrigo Fonseca over 10 years
    i use a controller separately from my directive too(yes, there is specific cases that you would do this), but this doesn't make any sense when you want to use that same data in several other places of your app.
  • Index
    Index over 10 years
    @Omegalen I'm a bit confused as to what is the problem. If you are going to manipulate your data inside an directive you need either a directive scoped controller or a link function. If however you mean that you apply the directive outside the original controllers (in my example, "MyController") scope you should create a service to share the data across your app. Let me know if that's the case.
  • Omegalen
    Omegalen over 10 years
    @KGChristensen The latter is correct. I apply the directive outside of the original controllers. So in this case, I should use a service?
  • Omegalen
    Omegalen over 10 years
    I selected this as the best answer because it was the most comprehensive. Thanks Brandon. One thing to note before I could get this to work: I had to register a listener callback ($watch) to the foreach function in the directive. Before I did this, the directive was not registering scope.boulders as being a defined object, but when I would check the scope object, I would see the array stored. I found out it was because promise runs asynchronously-- meaning that my directive was being run through before it could register the change to scope.
  • Michelle Tilley
    Michelle Tilley over 10 years
    No problem. Your 'watch' issue sounds funky to me; if you're interested, I'd be happy to take a look-you can find my contact info on my SO profile.
  • Michelle Tilley
    Michelle Tilley over 10 years
    @Omegalen After thinking about this, you're of course right. The locations property of the scope is created asynchronously, and thus the directive must fetch them asynchronously. I'll update the answer with the necessary code.
  • Index
    Index over 10 years
    @Omegalen Yes, you would then definitely be best of using a service to share the data across your app. From there it's simply a matter of injecting your service in your controllers / directives / what-have-you and request your data. Provided you set up your service correctly, it should only fetch data once and store it for later use.
  • Kevin Cloet
    Kevin Cloet over 9 years
    What if your directive isn't a 'child' of your SomeController? How do you pass your data then? I re-use the same directive on difference parts of the page and I want to manupilate the data from anywhere.
  • Aniket Sinha
    Aniket Sinha over 9 years
    Many a times, when I log the passed object, I get undefined. Is watching the only way to overcome this?
  • James Barracca
    James Barracca almost 9 years
    First and foremost, @BinaryMuse , your response was a huge help. Thank you! I was wondering if you could elaborate on the last piece with more of an example use case. I'm trying to understand "why" someone would use the map directive. Just trying to picture this particular directive in a real example. (Hope this makes sense!)
  • Michelle Tilley
    Michelle Tilley almost 9 years
    @JamesBarracca As I mentioned in the answer, directives abstract over DOM creating and manipulation. You don't want your controllers or services figuring out how to interact with the Leaflet API, creating/attaching DOM nodes, etc. You want them to deal with data and you send raw data to directives to turn them into useful DOM based on that data.
  • fodma1
    fodma1 almost 8 years
    I agree this is a great answer, my question is how well it plays together with the component architecture? As far as I know the operate exclusively with directives and their own controller
  • Michelle Tilley
    Michelle Tilley almost 8 years
    @fodma1 I would probably still not make them the same directive; e.g. in React I would either have one component for data loading and one for displaying the data (HOC ftw?) or move the data loading outside of components into something higher level (e.g. Flux, Redux, etc.)
  • Matt
    Matt over 7 years
    I noticed that the first comment on this thread discusses using $watch. Since this question is older now, Angular 1.5.3 addressed this issue by providing 4 Event Listeners for the newer module.component() which is simply a directive that is restricted as an Element. For more info, check out - blog.thoughtram.io/angularjs/2016/03/29/…