1

I'm new to angular and am struggling to get to understand how to use a Service to pass data from one controller to another.

So far with with Angular, I have been able to understand how to call controllers and pass data to them from within HTML. That's been especially fine in order to set up a Map Controller, whereby I look through a list of results that comes from PHP, and send data from those results into my controller, to create markers on a map.

What has me completely stumped, is having one controller that generates a nested object via a ElasticSearch call and then passing that data to my Map Controller.

Initially I tried looping through the results via ng-repeat, and while I was able to output the results, my MapController wasn't able to read those results in the HTML as they were within the confines of my IndexController (which was outputting the data). Well I assume that was the case.

This is what I tried, and while it outputted the data, I could never read it in MapController.

<div ng-controller="IndexController">
    <div ng-repeat="r in listingsArea.results">
        <div class="component home-listings"
            data-id="{{r.id}}"
            data-url="{{r.url}}"
            data-lat="{{r.lat}}"
            data-lng="{{r.lon}}"
            data-address="{{r.address}}"
            data-price="{{r.price}}"
        ></div>
    </div>
</div>

I've read that the best way to pass data from one controller to another is via a service. I've followed a lot of documentation here, but despite this I am obviously missing something as it is not working.

This is what I have thus far:

ResultsService.js

App.factory("resultsService", function() {

var loadListingsArea = {};

return {
    getLocations: function() {
        return loadListingsArea
    },

    setLocations: function(loc) {
        loadListingsArea = loc;

IndexController.js

App.controller('IndexController', function($scope, resultsService) {

$scope.listingsArea = [];

$scope.loadListingsArea = function() {
    $http.get($window.location + '/listingsarea')
        .success(function(data) {
            $scope.listingsArea = data;
        }
    );
}

$scope.$watch(function() {
    return $scope.loadListingsArea;
}, function() {
    resultsService.setLocations($scope.loadListingsArea);
});
});

MapController.js (just trying to dump the object at this stage, not including my very long code base for the google map)

App.controller('MapController', function($scope, resultsService) {

$scope.locations = resultsService.getLocations();

alert($scope.locations);

This is a sample of what my object looks like when dumped from indexcontroller.js

{"count":4,
"results":[
      {"id":"1153292",
       "url":"/this-is-my-slug/1153292",
       "lat":"-37.822034",
       "lon":"144.969553",
       "address":"1302/430 Place Road",
       "price":"$2,350,000",
       "hero":"some-image-here.jpg"}
]};
Ashkas
  • 117
  • 1
  • 2
  • 13

2 Answers2

1

The $http service returns a promise. Store that. No need to do the $q.defer stuff.

App.factory("resultsService", function() {

  var self = this;
  self.listingsPromise = undefined;

  return {
      loadLocations: function(url) {
           self.listingPromise = $http.get(url);
           return self.listingPromise;
      },
      getLocations: function() {
        return self.listingPromise;
      }
  );
}

The controller that initiates the retrieve can do this:

resultsService.loadLocations() // returns a promise
  .then(function(response){
    $scope.listingsArea = response.data;
  }) .catch ( function (error) {
    throw error;
  });

And the controller that just needs to get it can do this:

resultsService.getLocations()
  .then(function(response){
    $scope.listingsArea = response.data;
  }) .catch ( function(error) {
    throw error;
  });

Notice that both controllers need to check for errors.

Also notice that .then returns data differently than .success.

The AngularJS team has come to their senses and deprecated .success. We all should be using .then from now on.

For information on the deprecation of .success and more info see the latest AngularJS $http API Docs

georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • Tested and works a charm. From the way it runs, from my reading of this elements of the script wait for different parts to run before moving onto the next part. – Ashkas Nov 21 '15 at 06:14
  • getLocations will return an empty object if called before loadLocations, and calling .then on it will fail. So when the second controller is included but the first is not, this will fail. Mine returns a promise either way and the .then will not fail, but also will not be resolved if loadLocations is never called. – mgiesa Nov 21 '15 at 06:35
  • @mgiesa Look at [Is this a “Deferred Antipattern”?](http://stackoverflow.com/questions/30750207/is-this-a-deferred-antipattern/30757201#30757201) – georgeawg Nov 21 '15 at 12:49
  • Not entirely applicable because of the extra requirement here of accessing the data from a second controller. To add safety you would have to check in getLocations that self.listingPromise is in fact a promise, and if it isn't you would have to make one to return. You could change the method to lazy load, so if the promise doesn't exist yet you call the load function and get the promise. Or call the load method from both controllers and if the promise already exists return it instead of making a new request. But as is, your code will cause unhandled errors. – mgiesa Nov 21 '15 at 13:53
  • That will still return the empty object becuase empty objects are truthy. Try doing a typeof, like if (self.listingPromise && typeof self.listingPromise.then === 'function') return self.listingPromise. or initialize listingPromise with null instead of {} – mgiesa Nov 21 '15 at 14:29
0

Move the $http.get into a function in your service. Edited to use $q.deferred instead of returning the $http's promise.

App.factory("resultsService", function() {

  var self = this;
  self.deferred = $q.defer();
  self.listingsArea = {};

  return {
      loadLocations: function() {
          $http.get($window.location + '/listingsarea')
              .then(function(data){
                 self.listingsArea = data;
                 self.deferred.resolve(self.listingsArea);
              },
              function(e){
                self.deferred.reject(e);
              });
          return self.deferred.promise;
      },
      getLocations: function() {
        return self.deferred.promise;
      }
  );
}

The controller that initiates the retrieve can do this:

resultsService.loadLocations() // returns a promise
  .then(function(data){
    $scope.listingsArea = data;
  });

And the controller that just needs to get it can do this:

resultsService.getLocations()
  .then(function(data){
    $scope.listingsArea = data;
  });

This may require some extra massaging but it should give you a direction to head in.

Also note that sharing the object this way means you can edit the same object from either controller. To avoid this, pass around an angular.copy of the data object instead.

mgiesa
  • 1,023
  • 7
  • 9
  • Hmmm something is breaking along the way with this, as by using your suggestion, the mapcontroller never fully loads and thus doesn't execute. Issue seems to be in the indexcontroller, as commenting out the code there then ensures that it executes. – Ashkas Nov 21 '15 at 02:34
  • Found that one, had to define $http and $window in the function. Still not 100% sure whether the full object is being shared though. – Ashkas Nov 21 '15 at 02:59
  • I do something similar to this in my app. I have error callbacks and I don't put my data on $scope because I use the "controller as" syntax, that's about the only difference I can think of. This should share the object the way you need it to. – mgiesa Nov 21 '15 at 03:11
  • Fair enough. Upon further inspection, I've been able to confirm that the service is retrieving the object, that the object is being passed to the indexController, but it's not making it to the mapController. At that point, getLocations is always returning null. – Ashkas Nov 21 '15 at 03:46
  • Try putting the listingArea var in the service on self, see my edit. If that doesn't do it, it might be a race condition. Are you instantiating both controllers at the same time, or does one replace the other (like of you were using ngView)? If you're calling the load and then calling the get before any data is returned the second controller would get null. To get around this you could put the data in a wrapper object and store/share the wrapper object. This way once data is returned and put into the wrapper object it will update both controllers – mgiesa Nov 21 '15 at 03:51
  • Actually you're right the controllers are being loaded at the same time, so the 2nd controller is loading before the first one is finished, hence the object is returning null. I wasn't aware that was the case with Angular (like I said still learning the ropes). RE the wrapper object - I thought that was the purpose of the service? To store the object for sharing with another controller. Is there a way of caching objects in the service? – Ashkas Nov 21 '15 at 04:20
  • You need to send promises to both controllers. Stripping a promise in the first controller and expecting the second to access it will cause the problems you having. Send promises to both controllers. – georgeawg Nov 21 '15 at 04:33
  • I thought about that, but I was hoping to load the data specifically through IndexController, so as to only load that data on pages where IndexController is called. That's because I use the MapController on multiple pages and was hoping to avoid requesting data that is not required every time the MapCOntroller is called. – Ashkas Nov 21 '15 at 04:42
  • It appears that using $timeout in the MapController ensures that it waits briefly before calling the service (ie after IndexController has loaded data). – Ashkas Nov 21 '15 at 04:43
  • See my edits returning a promise to both controllers. Thanks @georgeawg for the suggestion – mgiesa Nov 21 '15 at 05:07
  • @mgiesa Better but that will only work for one shot. I will get you the correct answer in a moment. – georgeawg Nov 21 '15 at 05:13