0

The first time I visit a route with a resolve the request for the objects is not sent. The only way to visit the page is to make sure the route is correct in the url bar (typing or clicking a link) and refresh the page without caching (ctrl+shift+r in Firefox or ctrl+F5 in Chrome).

After I visit it the first time link will work.

app.config(['$stateProvider', function($stateProvider){

  $stateProvider.state('users', {
    templateUrl: '/app/Users/templates/users.html',
    controller: 'Users',
    resolve: {
      'users': function(Objects, $stateParams){
        return Objects.getUsers();
      }
    },
    url: '^/users'
  });

  $stateProvider.state('user', {
    templateUrl: '/app/Users/templates/user.html',
    controller: 'User',
    resolve: {
      'user': function(Objects, $stateParams){
        return Objects.getUser($stateParams.id);
      }
    },
    url: '^/users/:id/'
  });
}]);

app.factory('Objects', ['$http', '$q', function($http, $q){
  /* Retrieve objects once */
  var _cache = {};

  function cache(key, promiseGetterFn) {
    if (key in _cache) {
      return _cache[key];
    }
    else {
      var promise = promiseGetterFn();
      _cache[key] = promise;
      return promise;
    }
  }
  return {
    unsetKey: function(key){
      delete _cache[key];
    },
    getUsers: function() {
      return cache('users', function () {
        var deferred = $q.defer();
        $http.get(HOST + '/api/v1.0/users/all').then(
          function (result) {
            deferred.resolve(result);
          });
        return deferred.promise;
      });
    },
    /*
    getUsers: function(){
      return cache('users', function(){
        return $http.get(HOST + '/api/v1.0/users/all').success(
          function(data, status, headers, config){
            return data.users;
          }
        );
      });
    },
    */
/*
    getUsers: function(){
      return cache('users', function(){
        var deferred = $q.defer();
        return $http.get(HOST + '/api/v1.0/users/all').then(
          function(result){
            deferred.resolve(result.data.users);
          },
          function(status){
            deferred.reject(status);
          }
        );
        return deferred.promise;
      });
    },
*/
    getUser: function(id){
      return cache('user_' + id, function(){
        var deferred = $q.defer();
        return $http.get(HOST + '/api/v1.0/user/' + id).then(
          function(result){
            deferred.resolve(result.data.user);
          },
          function(status){
            deferred.reject(status);
          }
        );
        return deferred.promise;
      });
    },
  };
}]);

app.run(['$rootScope', '$location', 'LocalService', function($rootScope, $location, LocalService){

  $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
    if (!toState.publicAccess && !LocalService.get('loggedIn')){
      /* Store the route they were trying to access */
      LocalService.set('next', $location.path());

      $location.path('/login');
    }
  });
}]);

Redirect after login code

app.factory('AuthInterceptor', ['$q', '$injector', '$location', 'LocalService', function($q, $injector, $location, LocalService){
  /* Send Authorization in the header of each http request if there is a token */
  return {
    request: function(config){
      if (LocalService.get('token')){
        /* Using btoa to do Base64 */
        /* LocalService.password is only used on login to get token and will be empty ('') when using the token */
        config.headers.Authorization = 'Basic ' + btoa(LocalService.get('token') + ':' + LocalService.get('password'));
      }
      return config;
    },
    responseError: function(response){
      if(response.status === 401 || response.status === 403){
        /* Log the user out */
        LocalService.unset('loggedIn');
        LocalService.unset('token');
        LocalService.unset('user');
        $location.path('/login');
      }
      return $q.reject(response);
    }
  };
}]);

app.config(['$httpProvider', function($httpProvider){
  $httpProvider.interceptors.push('AuthInterceptor');
}]);

app.run(['$rootScope', '$location', 'LocalService', function($rootScope, $location, LocalService){

  $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
    if (!toState.publicAccess && !LocalService.get('loggedIn')){
      /* Store the route they were trying to access */
      LocalService.set('next', $location.path());

      $location.path('/login');
    }
  });
}]);

app.controller('Login', ['$scope', '$http', '$location', 'growl', 'LocalService',
  function($scope, $http, $location, growl, LocalService){
    $scope.email = '';
    $scope.password = '';

    $scope.submitLogin = function submitLogin(){
      LocalService.set('token', $scope.email);
      LocalService.set('password', $scope.password);
      $http.get(HOST + '/api/v1.0/token').
        success(function(data, status, headers, config) {
          LocalService.set('token', data.token);
          LocalService.set('loggedIn', true);
          LocalService.set('password', '');
          /* Set current user */
          $http.get(HOST + '/api/v1.0/authenticate').then(function(result) {
            LocalService.set('user', JSON.stringify(result.data));
            if (LocalService.get('next')){
              var next = LocalService.get('next');
              LocalService.unset('next');
              console.log(next);
              $location.path(next);
            }
            else{
              $location.path('/');
            }
          });
        }
      ).
      error(function(data, status, headers, config) {
          /* invalid credentials growl */
          growl.addErrorMessage('Invalid username or password.');
        }
      );
    };
  }
]);
Siecje
  • 3,594
  • 10
  • 32
  • 50
  • No console error, just no request. – Siecje Jul 25 '14 at 14:10
  • just remember to do authentication on the server-side as well... – mb21 Jul 28 '14 at 14:56
  • I'm doing authentication on the serverside and that works as long as I refresh with cache, or its the second or more time. – Siecje Jul 28 '14 at 15:27
  • It might not be related to your issue, but your code can be shorter + simpler if a) You use promise chaining, so instead of creating a promise via `$q.defer`, use the return values of the success + error callbacks; b) You use a library function like [memoize](http://lodash.com/docs#memoize) to do your caching or use [Angular $http caching](https://docs.angularjs.org/api/ng/service/$http#caching) – Michal Charemza Jul 30 '14 at 07:48

3 Answers3

0

My first thought on this would be that the resolved objects are only resolved on hard load. Once the app has been instantiated from an index.html, the partial views may not be registering that the objects are promises. I would try making the returned objects from the factory (the api) actual promises. Now I have never seen a promise quite like yours, but I do not see any 'deferred.resolve' or a 'deffered.reject'. For instance:

 return {
    getBundles: function(){
      return cache('bundles', function(){
        var deffered = $q.defer
        return $http.get(HOST + '/api/v1.0/bundles/all').success(
          function(data, status, headers, config){
            deferred.resolve(data.bundles);
          }.error(function(status){
            deferred.reject(status);
        )};
        return deferred.promise;
      });
    },
  }

In doing this, I would also recommend that you return the objects to a javascript object before binding it to the view. This would be done in the controller with the 'bundles' injected into the controller.

var thisBundle = bundles.data.bundles;
thisBundle.then(function(data){
    $scope.bundles = data;
});

Another solution I found was to wrap all of your items with a resolve in the routing. Try this:

resolve: {
        'allProducts' : function(){
            var theResolvePromise = $q.defer();
            theResolvePromise.resolve({
                bundles: ['Objects', function(Objects){
                  return Objects.getBundles();
                }],
                products: ['Objects', function(Objects){
                  return Objects.getProducts();
                }],
                technologies: ['Objects', function(Objects){
                   return Objects.getTechnologies();
                }],
                deliveryCategories: ['Objects', function(Objects){
                  return Objects.getDeliveryCategories();
                }],
             });
             return theResolvePromise.promise;
         };
      }
    }).

Then you would access this in the controller with the params passed through. Retrieved from: http://www.undefinednull.com/2014/02/17/resolve-in-angularjs-routes-explained-as-story/

Hope this helps,

Patrick

Maximus
  • 56
  • 3
  • I'm getting deferred is not defined. I think inside the .error(). I also ended the success before the .error https://dpaste.de/aQRJ – Siecje Jul 28 '14 at 18:59
  • Did you instantiate the '$q' for the function that wraps the return? Make sure you have the injection. – Maximus Jul 28 '14 at 19:24
  • The deffered above shows as not being used by JSHint. I am not 100% on where the return should go, usually it goes inside. I do have the '$q' dependency. – Siecje Jul 28 '14 at 20:06
  • I updated the answer with another solution and a bit more explanation on the return. See if that works. I also attached a URL to a site that explains it fairly well that I used when learning. – Maximus Jul 28 '14 at 22:01
  • The `$http` calls return promises. – craigb Jul 29 '14 at 15:58
0

Try this. I didn't test it, so it might not be perfect, but maybe it will help point you in the right direction.

app.factory('Objects', ['$http', function($http){
  var _cache = {};

  function cache(key, getterFn) {
    if (_cache[key] == null) {
      _cache[key] = getterFn();
      _cache[key].then(function (result) {
          _cache[key] = result;
      });
    }

    return _cache[key];
  }

  return {
    unsetKey: function(key){
      delete _cache[key];
    },
    getUsers: function() {
      return cache('users', function () {
        return $http.get(HOST + '/api/v1.0/users/all');
      });
    },
    getUser: function(id){
      return cache('user_' + id, function() {
        return $http.get(HOST + '/api/v1.0/user/' + id).then(function (result) {
          return result.data.user;
        });
      });
    },
  };
}]);

$http.get returns a promise that resolves when the request completes. Returning a value from a .then() function will pop that value into the next .then() in the chain. UI-Router's resolves automatically unwrap the promise if given one. If given anything else, it just returns it. Promises don't automatically unwrap in Angular 1.2+ so you need to unwrap it yourself when you're working in your cache.

Ben Wilde
  • 5,552
  • 2
  • 39
  • 36
  • That seems to work, except it breaks my redirect after login. I've added the code for that in my post. – Siecje Aug 05 '14 at 15:51
0

Your question is 2 part.

1) Why my first resolve not resolving the resolve for the state ?

The first time you return a "promise to fulfill a promise of fulfilling data". It is like a chained promise. It will take one extra $digest cycle to resolve this vs. if you returned a promise to get the data. $http.get() returns a promise.

In AngularJS the results of promise resolution are propagated asynchronously, inside a $digest cycle. So, callbacks registered with then() will only be called upon entering a $digest cycle.

Also check this out

Angular JS: Chaining promises and the digest cycle

2) What can you change to fix the problem ?

You can do 2 things Just return the $http.get() because this is itself a promise. Why cache a promise object ? You should cache the actual data. Also with $resource and $http you can pass {cache: true} and the results will get cached. Alternatively if you like to take control you may want to use $cacheFactory. In this case you can place the actual results in the cache and not the promise object.

Community
  • 1
  • 1
bhantol
  • 9,368
  • 7
  • 44
  • 81
  • If I add {cache: true} to the $http.get() requests how can I delete the cache when I add a new item in the database and no longer want to use the cached result? – Siecje Aug 05 '14 at 15:53
  • var defaultCache = $cacheFactory('$http'); However you should your own cache. My point was to use $cachefactory rather than reinvent. You can replace the default cache with your own so that you can have more control. As per Angular documentation for $http. You can change the default cache to a new object (built with $cacheFactory) by updating the $http.defaults.cache property. All requests who set their cache property to true will now use this cache object. Do this in config/run at your application level one time. – bhantol Aug 05 '14 at 16:43