9

I'm trying to make a single $http request to get one of my JSON files and use the data across all my controllers.

I saw on egghead.io how to share data across multiple controllers, and I've also read this StackOverflow question: "Sharing a variable between controllers in angular.js".

However, the answers there don't use the $http module. When using $http, the controllers don't have the data to work on, and by the time the response is received it's already too late.

I then found the method $q.defer and this question on StackOverflow: "AngularJS share asynchronous service data between controllers"

The solution posted there works fine, BUT it has two issues:

  1. Each controller will trigger the $http request to obtain the same data already used in another controller; and,
  2. If I try to manipulate the data received I have a then function.

Below you can see my code:

controllers.js

'use strict';

/* Controllers */

function appInstallerListCtrl($scope, Data) {
  $scope.apps = Data;
}

function appInstallerDetailCtrl($scope, $routeParams, Data) {
  $scope.appId = $routeParams.appId;
  $scope.apps = Data;  
  console.log($scope.apps); // <-- then function
  console.log(Data); // <-- then function with $vv data returned but I can't access it

  for (var i in $scope.apps) // <--- no way, baby!
    console.log(i);
}

app.js

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

app.factory('Data', function($http, $q) {
  var defer = $q.defer();
  $http.get('apps.json').then(function(result) {
    defer.resolve(result.data.versions.version);
  });
  return defer.promise;
});

app.config(['$routeProvider', function($routeProvider) {
  $routeProvider.
    when('/app', {templateUrl: 'partials/app-list.html',   controller: appInstallerListCtrl}).
    when('/app/:appId', {templateUrl: 'partials/app-detail.html', controller: appInstallerDetailCtrl}).
    otherwise({redirectTo: '/app'});
}]);

What I'd like to have is that when launching the app, the $http request will be performed and the response will be used throughout the app across all controllers.

Thanks

4 Answers 4

14

I like to store my data in the service, and return a promise to the controllers, because usually you need to deal with any errors there.

app.factory('Data', function($http, $q) {
   var data = [],
       lastRequestFailed = true,
       promise;
   return {
      getApps: function() {
         if(!promise || lastRequestFailed) {
            // $http returns a promise, so we don't need to create one with $q
            promise = $http.get('apps.json')
            .then(function(res) {
                lastRequestFailed = false;
                data = res.data;
                return data;
            }, function(res) {
                return $q.reject(res);
            });
         }
         return promise;
      }
   }
});

.controller('appInstallerListCtrl', ['$scope','Data',
function($scope, Data) {
    Data.getApps()
    .then(function(data) {
        $scope.data = data;
    }, function(res) {
        if(res.status === 500) {
            // server error, alert user somehow
        } else { 
            // probably deal with these errors differently
        }
    });
}]);

Any callbacks that are registered after a promise has been resolved/rejected will be resolved/rejected immediately with the same result/failure_reason. Once resolved/rejected, a promise can't change (its state). So the first controller to call getApps() will create the promise. Any other controllers that call getApps() will immediately get the promise returned instead.

Sign up to request clarification or add additional context in comments.

8 Comments

+1, this is (i think) the finest practice for sync large app. Thank, and as often on stackoverflow, your right :)
Binding to promises doesn't work in the newer versions(1.2+ i think). Use Data.getApps().then(function(result){//save result here in $scope})
@shrutyzet, thanks. I finally got around to updating my answer.
But if u run Data.getApps().then(function(data) { .. }); multiple times, you get multiple GET requests. How to stick with only one request?
@Anze, I see. If you change the if statement to if(!promise) then you only get one GET request. It seems that with two controllers, the promise gets resolved before lastRequestFailed is set, so this introduces a race condition.
|
1

Since you are using a promise, to access the data returned by promise use the callback syntax

function appInstallerDetailCtrl($scope, $routeParams, Data) {
  $scope.appId = $routeParams.appId;
   Data.then(function(returnedData) {
        $scope.apps=returnedData;
        console.log($scope.apps);
        for (var i in $scope.apps)
           console.log(i)   
   });   
}

Make sure this

defer.resolve(result.data.versions.version);

resolve returns array, for the above code to work. Or else see what is there in data and ajust the controller code.

3 Comments

Thanks but the code is not correct. I get an error at: Uncaught SyntaxError: Unexpected token { after the Data.then and there is still the ) bracket.
See the documentation on $q here docs.angularjs.org/api/ng.$q , i just highlighted the basic idea. I have tried to fix my code too.
wow it works! thank you very much and I'll check out the doc you highlighted.
1

I found the way not sure weather it is a best approach to do it or not.

In HTML

<body ng-app="myApp">
  <div ng-controller="ctrl">{{user.title}}</div>
  <hr>
  <div ng-controller="ctrl2">{{user.title}}</div>
</body>

In Javascript

 var app = angular.module('myApp', []);
   app.controller('ctrl', function($scope, $http, userService) {
      userService.getUser().then(function(user) {
        $scope.user = user;
      });
    });

   app.controller('ctrl2', function($scope, $http, userService) {
      userService.getUser().then(function(user) {
        $scope.user = user;
      });
    });

   app.factory('userService', function($http, $q) {
    var promise;
    var deferred = $q.defer();
      return {
        getUser: function() {
          if(!promise){     
          promise = $http({
              method: "GET",
              url: "https://jsonplaceholder.typicode.com/posts/1"
            }).success(function(res) {
                data = res.data;
              deferred.resolve(res);
            })
            .error(function(err, status) {
              deferred.reject(err)
            });
          return deferred.promise;
          }
          return deferred.promise;
        }
      }
    });

This will exactly make only 1 HTTP request.

Comments

0

My issue was that I didn't want to wait for resolve before loading another controller because it would show a "lag" between controllers if the network is slow. My working solution is passing a promise between controllers via ui-router's params and the data from promise can be loaded asynchronously in the second controller as such:

app.route.js - setting the available params to be passed to SearchController, which shows the search results

        .state('search', {
            url: '/search',
            templateUrl: baseDir + 'search/templates/index.html',
            controller: 'SearchController',
            params: {
                searchPromise: null
            }
        })

landing.controller.js - controller where the user adds search input and submits

    let promise = SearchService.search(form);
    $state.go('search', {
        searchPromise: promise
    });

search.service.js - a service that returns a promise from the user input

    function search(params) {
        return new Promise(function (resolve, reject) {
            $timeout(function() {
                resolve([]) // mimic a slow query but illustrates a point
            }, 3000)
        })
    }

search.controller.js - where search controller

    let promise = $state.params.searchPromise;

    promise.then(r => {
        console.log('search result',r);
    })

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.