Resolve 2 arrays before anything on the page with a promise - javascript

I have 2 arrays, sports and leagues and I want those arrays to become resolve before anything on the page, I will paste all of my code regarding those arrays I just mentioned. I need to do this due to an issue I am having with the Angular filters.
this is my html
<div ng-repeat="sport in sportsFilter = (sports | filter:query)">
<div ng-if="sport.leagues.length">
<!--first array-->
{{sport.name}}
</div>
<div ng-repeat="league in sport.leagues">
<!--second array-->
{{league.name}}
</div>
</div>
controller
.controller('SportsController', function($scope, $state, AuthFactory,
SportsFactory, Sports) {
$scope.sports = [];
$scope.sportPromise = Sports;
AuthFactory.getCustomer().then(function(customer) {
$scope.customer = customer;
SportsFactory.getSportsWithLeagues(customer).then(function(sports) {
$ionicLoading.hide();
if (sports.length) {
$scope.sportPromise = Sports;
$scope.sports = sports;
}else {
AuthFactory.logout();
}
}, function(err) {
console.log(err);
});
}
});
$scope.isSportShown = function(sport) {
return $scope.shownSport === sport;
};
});
and here de app.js so far, I thought with this I was resolving the arrays, actually the must important is the array named leagues, but still is giving me troubles
.state('app.sports', {
url:'/sports',
views:{
menuContent:{
templateUrl:'templates/sportsList.html',
controller:'SportsController',
resolve: {
Sports: function(SportsFactory, AuthFactory, $q) {
var defer = $q.defer();
AuthFactory.getCustomer().then(function(customer) {
SportsFactory.getSportsWithLeagues(customer).then(function(sports) {
var sportLeagues = _.pluck(sports, 'leagues'),
leaguesProperties = _.chain(sportLeagues).flatten().pluck('name').value();
console.log(leaguesProperties);
defer.resolve(leaguesProperties);
});
});
return defer.promise;
}
}
}
}
})
UPDATE:
the page is loading and I my filter is getting the array leagues empty, so is not searching thru to it, so I need that array to load first than the filters.

Here's how this could work at a high-level, including avoiding some mistakes you are making.
Mistakes:
You don't need to use $q.defer when the API you are using is already returning a promise. Just return that promise. What you are doing is called an deferred anti-pattern.
resolve is used when you need to do something (like authentication) before you are hitting a particular state. You are under-using the resolve by not resolving the customer, and instead doing this in the controller.
resolve property of $stateProvider can accept other resolves as parameters.
With these out of the way, here's how it could work:
.state('app.sports', {
resolve: {
customer: function(AuthFactory){
return AuthFactory.getCustomer();
},
sports: function(SportsFactory, customer){
return SportsFactory.getSportsWithLeagues(customer);
},
leagues: function(sports){
var leagueProperties;
// obtain leagueProperties from sports - whatever you do there.
return leagueProperties;
}
}
});
Then, in the controller you no longer need AuthFactory - you already have customer:
.controller('SportsController', function($scope, customer, sports, leagues){
$scope.sports = sports;
})

As per request in the comments of New Dev's answer the same only using array notation so that the code remains minifiable:
.state('app.sports', {
resolve: {
customer: ['AuthFactory', function(AuthFactory){
return AuthFactory.getCustomer();
}],
sports: ['SportsFactory', 'customer', function(SportsFactory, customer){
return SportsFactory.getSportsWithLeagues(customer);
}],
leagues: ['sports', function(sports){
var leagueProperties;
// obtain leagueProperties from sports - whatever you do there.
return leagueProperties;
}]
}
});
A little explanation to go with that, when you minify this:
function (AuthFactory) {
return AuthFactory.getCustomer();
}
You get something like this:
function (_1) {
return _1.getCustomer();
}
Now it will try to inject _1 which is not defined so the code will fail. Now when you minify this:
['AuthFactory', function(AuthFactory){
return AuthFactory.getCustomer();
}]
You'll get this:
['AuthFactory', function(_1){
return _1.getCustomer();
}]
And that will keep working because now _1 is assigned to AuthFactory because angular injects the first parameter in the function with the first string in the array.
Reference: https://docs.angularjs.org/guide/di (see: inline array notation)

Related

working with deferred results in angularjs

Can't seem to get my head over the concept of 'promise' in AngularJS, I am using the restangular library to fetch a resource over REST, however I always get null results. Here's the code
.service('CareersService', [ 'Restangular', '$sce', function(Restangular, $sce){
var vacancies = [];
var result;
this.getVacancies = function() {
Restangular.all('job_posts').getList({active: 'true'}).then(function(job_posts){
job_posts.forEach(function(job_post){
vacancies.push(_.pick(job_post,['id','title','min_experience','max_experience','location']));
})
})
return vacancies;
}
this.getVacancy = function(job_id){
Restangular.one('job_posts',job_id).get().then(function(job_post){
result = _.pick(job_post, 'title','min_experience','max_experience','location','employment_type','description');
var safe_description = $sce.trustAsHtml(result.description);
var emp_type = _.capitalize(result.employment_type);
_.set(result, 'description', safe_description);
_.set(result, 'employment_type', emp_type);
});
return result;
}
}]).controller('DetailsCtrl', ['$scope' ,'$stateParams', 'CareersService' ,function($scope, $stateParams, CareersService) {
$scope.data.vacancy = { title: 'Loading ...', contents: '' };
$scope.data.vacancy = CareersService.getVacancy($stateParams.job_id);
}])
and then in view
<div class="container">
<a ui-sref="careers" class="btn btn-primary">Show All</a>
<div class="row">
<h2>{{ data.vacancy.title }}</h2>
<p>{{ data.vacancy.min_experience }}</p>
<p>{{ data.vacancy.max_experience }}</p>
<p>{{ data.vacancy.location }}</p>
<p>{{ data.vacancy.employment_type }}</p>
<p ng-bind-html="data.vacancy.description"></p>
</div>
</div>
Am I missing something in the way to use promises?
Update
here's the updated code thanks to all the help I got here,
this.getVacancies = function() {
Restangular.all('job_posts').getList({active: 'true'}).then(function(job_posts){
job_posts.forEach(function(job_post){
vacancies.push(_.pick(job_post,['id','title','min_experience','max_experience','location']));
})
return vacancies;
})
}
this.getVacancy = function(job_id){
Restangular.one('job_posts',job_id).get().then(function(job_post){
vacancy = _.pick(job_post, 'title','min_experience','max_experience','location','employment_type','description');
...
return vacancy;
});
}
}])
And in controllers
CareersService.getVacancy($stateParams.job_id).then(function (vacancy){
$scope.data.vacancy = vacancy;
});
and
CareersService.getVacancies().then(function (vacancies){
$scope.data.vacancies = vacancies;
});
I now get the error
Cannot read property 'then' of undefined
At the line
CareersService.getVacancies().then(function(vacancies) {
Restangular makes an API call over a http, and once it make a call it returns underlying promise object. And inside .then function of it you can get the data responded by API.
So here you are making an async call and considering it to happen it in synchronous way like you can see you had returned result/vacancies array from Restangular call, in that way result/vacancies is always going to be empty.
In such you should return a promise from a service method. And return appropriate formatted data from promise so that you can chain that promise in controller as well(by retrieving a data).
Service
this.getVacancies = function() {
//returned Restangular promise
return Restangular.all('job_posts').getList({
active: 'true'
}).then(function(job_posts) {
job_posts.forEach(function(job_post) {
vacancies.push(_.pick(job_post, ['id', 'title', 'min_experience', 'max_experience', 'location']));
});
//return calculated result
return vacancies;
})
}
this.getVacancy = function(job_id) {
//returned Restangular promise
return Restangular.one('job_posts', job_id).get().then(function(job_post) {
result = _.pick(job_post, 'title', 'min_experience', 'max_experience', 'location', 'employment_type', 'description');
var safe_description = $sce.trustAsHtml(result.description);
var emp_type = _.capitalize(result.employment_type);
_.set(result, 'description', safe_description);
_.set(result, 'employment_type', emp_type);
//returned result to chain promise
return result;
});
}
As I said now you can easily chain promise inside controller by having .then function over service method call.
CareersService.getVacancy($stateParams.job_id).then(function(result){
$scope.data.vacancy = result;
});
Update
The syntax without .then would work, but you need to make small change in it by adding .$object after a method call.
$scope.data.vacancy = CareersService.getVacancy($stateParams.job_id).$object;
$object is property which added inside promise object by Restangular. While making an API call, at that time it makes $scope.data.vacancy value as a blank array ([]) and once server respond with response, it fills that object with response received by server. Behind the scene it only updates the value of $object property which automatically update $scope.data.vacancy value.
Same behaviour is there in $resource of ngResource.
I wanted to also put down that when you're chaining promise, that time you have to explicitly handle error case. Whereas in current code you haven't handle such failure condition. So I'd suggest you to go for that as well by adding error function inside Restangular REST API call. and do use $q.reject('My error data, this can be object as well').
You are making a async call to get the result. So you need either a callback or promise to handle this. One option with minimum code change is to make the service to return promise and in the controller get the result via then
.service('CareersService', ['Restangular', '$sce', function(Restangular, $sce) {
var vacancies = [];
var result;
this.getVacancies = function() {
Restangular.all('job_posts').getList({
active: 'true'
}).then(function(job_posts) {
job_posts.forEach(function(job_post) {
vacancies.push(_.pick(job_post, ['id', 'title', 'min_experience', 'max_experience', 'location']));
})
})
return vacancies;
}
this.getVacancy = function(job_id) {
return Restangular.one('job_posts', job_id).get().then(function(job_post) {
result = _.pick(job_post, 'title', 'min_experience', 'max_experience', 'location', 'employment_type', 'description');
var safe_description = $sce.trustAsHtml(result.description);
var emp_type = _.capitalize(result.employment_type);
_.set(result, 'description', safe_description);
_.set(result, 'employment_type', emp_type);
return result;
});
}
}]).controller('DetailsCtrl', ['$scope', '$stateParams', 'CareersService', function($scope, $stateParams, CareersService) {
$scope.data.vacancy = {
title: 'Loading ...',
contents: ''
};
CareersService.getVacancy($stateParams.job_id).then(function(result) {
$scope.data.vacancy = result;
});
}])
You are doing return outside of then try to move it inside in then function
this.getVacancies = function() {
Restangular.all('job_posts').getList({active: 'true'}).then(function(job_posts){
job_posts.forEach(function(job_post){
vacancies.push(_.pick(job_post,['id','title','min_experience','max_experience','location']));
})
return vacancies;
})
}

AngularJS communication between components through a template (with a promise)

I'm working on an app that has actor listings. There are tags and actors with a many to many relationships on the backend.
I have 2 components and I want to pass and unresolved promise between them through a template.
This is the actor-tag-detail component:
angular.module('actorTagDetail').component('actorTagDetail', {
templateUrl: 'static/partials/actor-tag-detail/actor-tag-detail.template.html',
controller: ['$routeParams', 'ActorTag',
function ActorTagDetailController($routeParams, ActorTag) {
var self = this;
self.actorTag = ActorTag.get({actorTagId: $routeParams.actorTagId})
}
]
});
The actorTag returns a JSON for the ActorTag object from the backend that looks like this:
{
"id": 37,
"name": "Oscar Winner",
"actors": [
664,
668,
670,
673,
678,
696
] }
I also have an actor-list component that is responsible for retrieving an actor list from the backend:
angular.module('actorList').component('actorList', {
templateUrl: 'static/partials/actor-list/actor-list.template.html',
bindings: {
pk: '='
},
controller: ['Actor',
function ActorListController(Actor) {
var self = this;
alert(self.pk)
self.actors = Actor.query();
self.orderProp = 'name';
}
]
});
In the template for actor-tag-details I'm trying to pass an attribute to the actor-list component.
actor-tag-detail.template.html:
<actor-list pk="$ctrl.actorTag.actors"></actor-list>
What I'm trying to achieve is to have access to the retrieved JSON data in the actor-list component. However I'm getting an 'unresolved object error' instead of the desired result when I try to alert(self.pk) in actor-list.component.This is happening because when alert(self.pk) runs, the data is not yet retrieved from the backend.
I do not know how to remedy the situation.
Is there a way to specify in the receiving component to await execution until the promise is resolved and the data received from the backend?
Or, am I'm doing it completely wrong and there is a better way to pass this information between components?
Use a watcher to listen the object changes:
function ActorListController($scope, Actor) {
var self = this;
$scope.$watch(function() {
return self.pk;
}, function(newValue, oldValue) {
if(angular.isObject(newValue) {
//alert(self.pk)
self.actors = Actor.query();
}
}
}

How to properly resolve multiple factories using Angularjs' UI.Router

Using ui.router this successfully directs me to the proper view and properly retrieves the data from my factory:
.state('calendar', {
url: '/calendar',
templateUrl: 'templates/calendar.html',
controller: 'calendarCtrl',
resolve: {
workoutData: ['WorkoutData', function(WorkoutData){ // USING A FACTORY (workoutDataFct.js)
return WorkoutData.get();
}]
}
})
but I now want to pull in data from another factory but I can't seem to just add a new factory like this:
resolve: {
workoutData: ['WorkoutData', function(WorkoutData){
return WorkoutData.get();
}],
exercises: ['Exercises', function(Exercises){ // USING FACTORY (exercisesFct.js)
return Exercises.get();
}]
}
My factory, exerciseFct.js, is included in my index.html just like workoutDataFct.js and I included the exercises the dependency in my controller calendarCtrl just like I added the workoutData dependency. (am I forgetting to do something?)
I don't get and console errors and but I am not routed to the calendar view. This makes me think that the resolve: is failing. How do I fix this?
To make things clear here are the available options in your case.
Option 1:
Wait for all promises to finish with $q.all.
resolve: {
delayedData: function($q, WorkoutData, Exercises) {
var WorkoutData = WorkoutData.get();
var Exercises = Exercises.get();
WorkoutData.$promise.then(function(response) {console.log('Resource 1 data loaded!')});
Exercises.$promise.then(function(response) {console.log('Resource 2 data loaded!')});
return $q.all([WorkoutData.$promise, Exercises.$promise]);
}
}
Working example can be found here.
Option 2:
As #ExplosionPills stated use object with resolve.
resolve: {
WorkoutData: ["WorkoutData", function (Gists) {
var WorkoutData = WorkoutData.get();
WorkoutData.$promise.then(function(response) {console.log('Resource 1 data loaded!')});
}],
Exercises: ["Exercises", function (Meta) {
var Exercises = Exercises.get();
Exercises.$promise.then(function(response) {console.log('Resource 2 data loaded!')});
}]
}
Working example can be found here.
Thanks #Explosion Pills for your inputs.

Filter not working after refreshing the page

I have this input search
<input type="search" ng-model="query">
<div ng-repeat="sport in sports | filter:query"></div>
which is working awesome after the user logs in, but within the same session if the user refreshes the page, the filter doesn't work anymore, if you type something in the search box, nothing happens, the array doesn't change.
this is the service
angular.module('myApp.services')
.factory('SportsFactory', function($http, $q,
AuthFactory, LeaguesFactory, LocalForageFactory, CONSTANT_VARS) {
return {
getSportsWithLeagues: function(customer) {
var _this = this,
defer = $q.defer(),
rejection = function(err) {
defer.reject(err);
},
sportsLength;
LocalForageFactory.retrieve(CONSTANT_VARS.LOCALFORAGE_SPORTS_LEAGUES)
.then(function(sportLeagues) {
if (!_.isNull(sportLeagues)) {
defer.resolve(sportLeagues);
}else {
_this.getSports(customer.agent).then(function(sports) {
sportsLength = sports.length;
LeaguesFactory.getLeagues({
sportIds: _.pluck(sports, 'id'),
lineProfile: customer.lineProfile,
betLimit: customer.betLimit
}).then(function(leagues) {
_.each(sports, function(sport) {
sport.leagues = _.filter(leagues, function(league) {
return sport.id === league.sport.id;
});
});
LocalForageFactory.set(CONSTANT_VARS.LOCALFORAGE_SPORTS_LEAGUES, sports);
defer.resolve(sports);
}, rejection);
}, rejection);
}
}, rejection);
return defer.promise;
}
}
}
and here a part of the controller
angular.module('myApp.controllers', [])
.controller('LinesCtrl', function($scope, customer,
SportsFactory, LinesFactory) {
$scope.query = '';
SportsFactory.getSportsWithLeagues(customer).then(function(sports) {
$scope.sports = sports;
});
}
I also have this $watch
$scope.$watch('query', function(val) {
console.log(val);
});
to check if the filter stops working after refreshing, but still works properly and you can see the logs in the console every time you type something in the finder.
So, what do you think is happening here ?
the issue is that every time you are refreshing the page, in the promise sportLeagues is returning an object, only an object with some other objects inside, as you are using lodash, just put defer.resolve(_.values(sportLeagues)); and that will be enough to return the values that you need. Tell me if you need to know something else

Is it possible to use angularjs cached resource method in a filter?

I have a property in the scope that has an id of external object, also I have a filter that expands this id into a full object like this:
{{ typeId | expandType }}
Filter:
.filter('expandType', ['TypeService', function (tsvc) {
return function (id) {
return tsvc.types.get({ id: id });
}
}])
where tsvc.types.get() is normal resource get method with added cache option.
.factory('TypeService', ['$resource', function ($resource) {
var typeResource = $resource('/api/types/:id', { id: '#id' }, {
get: { method: 'GET', cache: true, params: { id: '#id' } }
});
return {
types: typeResource
}
}])
As I understand angular runs additional digest after the fist one just to make sure that nothing changed. But apparently on the next digest the filter is returning a different object and I get the infdig error (digest is executed in infinite loop).
I hoped that if the resource is cached it will return the same object from cache all the time. I can confirm that there is only one trip to server while executing get() so the cache is working.
What can I do to make it work and use the filter to expand ids to full objects?
Although possible, it is usually not a good idea to bind promises to the view. In your case, filters are reevaluated on every digest, and quoting from https://docs.angularjs.org/api/ng/service/$http:
When the cache is enabled, $http stores the response from the server in the specified cache. The next time the same request is made, the response is served from the cache without sending a request to the server.
Note that even if the response is served from cache, delivery of the data is asynchronous in the same way that real requests are.
To clarify, ngResource uses $http internally.
You can still use the filter calling it from your controller:
app.filter('expandType', function ($http) {
return function (id) {
return $http.get('data.json');
};
});
app.controller('MainCtrl', function ($scope, expandTypeFilter) {
var typeId = 'hello';
expandTypeFilter(typeId).success(function (data) {
$scope.expandedTypeId = data[typeId];
});
});
Plunker: http://plnkr.co/edit/BPS9IY?p=preview.
With this approach, if the only reason you were caching the response was to avoid repeated calls to the server, you can now stop caching it so that it gets fresh data later on, but that depends on your needs, of course.
I really wanted to use a filter because it was used all over the app and I didn't want to clutter my controllers. At this point the solution I came out with looks as follows:
.filter('expandType', ['TypeService', function (tsvc) {
var cache = {};
return function (id) {
if (!id) {
return '';
}
var type = cache[id];
if (!type) {
tsvc.types.get({ id: id }).$promise.then(function (data) {
cache[id] = data;
});
cache[id] = {}
return cache[id];
}
else {
return type;
}
}
}])

Categories

Resources