How to use Firebase query in Angular factory? - angularjs

I'm trying to get the new Firebase queries working in my factory, but I'm stuck. I want to return the value of 'logo' out of my opponents table, based on the opponent name. I can get the example with console.log from the Firebase docs running:
function OpponentService($firebase) {
var factory = {};
var _url = 'https://somefirebaseproject.firebaseio.com/opponents';
var _ref = new Firebase(_url);
factory.getLogo = function(opponent) {
_ref.orderByChild('name').equalTo(opponent).on("child_added", function(snapshot){
console.log(snapshot.val().logo);
});
};
return factory;
}
When I call my factory with OpponentService.getLogo(opponentName) in my controller, I get the correct logo id in my console. But how can I return the value instead of sending it to my console? I want to store it in a variable like this: $scope.opponentLogo = OpponentService.getLogo(opponentName). I tried several variations with a return statement like this:
factory.getLogo = function(opponent) {
_ref.orderByChild('name').equalTo(opponent).on("child_added", function(snapshot){
return snapshot.val().logo;
});
};
But apparently I don't fully understand how factories work in Angular, because I get an undefined. Could it be that the value isn't available yet and I should use a promise in someway? Anyone who can point me in the right direction?

You're returning the value of logo from the anonymous function inside the Firebase on() call, but you're not returning anything from getLogo().
Returning a promise would be a good way to do this. This is how you retrieve the opponent logo with AngularFire, if there is no guarantee opponentName will be unique:
// getLogo() returns a promise that you can bind to the UI.
// When loading finishes, the binding will get the opponent's logo value.
factory.getLogo = function (opponentName) {
var opponentsArray = $firebase(_ref.orderByChild('name').equalTo(opponentName)).$asArray();
// $loaded() returns a promise, so we can use the then() method to add
// functionality when the array finishes loading and the promise resolves.
var r = opponentsArray.$loaded().then(function () {
// Now return the logo for the first matching opponent
return opponentsArray[0].logo;
});
return r;
};
If opponentName is unique, I would rethink the data structure so that you could use the opponentName as the key for opponents. Then you would be guaranteed to get a single opponent and could fetch it with:
var opponent = $firebase(_ref.child(opponentName)).$asObject();
If you're not using AngularFire, you can return a promise using Angular's $q service. The following should work:
factory.getLogo = function(opponent) {
$q(function (resolve, reject) {
function successCallback(snapshot) {
resolve(snapshot.val().logo);
};
function cancelCallback(error) {
reject(error); // pass along the error object
};
_ref.orderByChild('name').equalTo(opponent)
.on("child_added", successCallback, cancelCallback);
});
};
You can assign the result of getLogo() to a scope variable, and bindings will update in the UI when Firebase returns the value.

Related

Angularjs Factory value doesn't update in multiple views

I have this simple factory to get products list from data base and also to toggle if the product is favorite or not (based on user interaction).
function factoryProduct($http,$filter) {
var data = {};
data.list = [];
var service = {
getData: _getData,
toggleFav: _toggleFav
};
return service;
function _getData() {
return $http.get('my/url/get.php').then(function(res){
data.list = res;
return res;
});
};
function _toggleFav(value) {
/* data manipulation here... */
return $http.post('my/url/post.php', data).then(function(res){
if (res==1) {
return $filter('filter')(data.list)[index].inFav = value;
};
};
};
}
This is used in multiple views, such as home, category page, favorite list, wishlist, etc.. And thus it's used inside multiple controllers, where I inject the factory and then pass the data to the view.
The toggle function, since it's the same, it's called from within a directive, but is also simple, like this:
scope.toggleFav = function(data, index) {
/*data verification here*/
factoryProduct.toggleFav(value);
}
And in the controller, like this:
function MainCtrl(factoryProduct) {
var vm = this;
factoryProduct.getData().then(function(res){
vm.list = res;
})
}
function CategoryCtrl(factoryProduct) {
var vm = this;
/* category taken from url parameter */
factoryProduct.getData().then(function(res){
$filter('filter')(res, {category: urlParam});
vm.list = res;
})
}
I can get data properly, make the filter and show the correct product list on each view. I also can toggle the favorite and both, the filter and the database, are properly updated.
The problem
The problem starts when I need to change view. For example:
If I'm on the home page, set a product as favorite, go to the contact page and then comeback to the home page, the product I just set as favorite is now as 'non-favorite' item, even if it's favorite in the database and also was updated before going to the contact page.
I'm using this:
var data = {};
data.list = [];
Because I saw many answers saying to have a static array and only manipulate the data inside this array. But it's not working for me.
Any ideas?
There are numerous ways to set up storing data in factory for the duration of the life of the page.
Using a resolve in router can be very helpful to populate data stores. Then when controllers fire the data already exists.
Another one is to check if data exists and if it does return a $q.resolve(data).
If it doesn't exist you return the $http promise that first stores the data and then returns the stored object reference.
Another I just picked up on recently that is helpful if numerous parts of app may ask for same data before a single request has completed.
You can store the original promise made. You can use then() on that promise any time you want within the app lifecycle
app.factory('Factory', function($http){
var dataPromise=null, data=null;
function getData() {
if (!dataPromise) {
console.warn('NEW REQUEST BEING MADE')
dataPromise = $http.get('data.json').then(function(resp) {
// store the data
data = resp.data
// return data stored in factory
return data;
})
}else{
console.info('Existing promise being returned')
}
return dataPromise;
}
return {
getData: getData
}
});
This last approach will prevent 2 parts of the app making 2 simultaneous requests if the first one hasn't completed
A final approach that is used by $resource is to return an empty object that is stored in factory. Then when requests are completed they merge data into that object without breaking the original object reference. Then when that object is passed directly through to views angular watchers will catch changes and do updates
DEMO

Angular $resource - Data not being returned

I am quite beginner level with both JS and Angular, and I am trying to return data from an API and store it in $scope.
Once I've stored it, I want to loop over each item and output it into the page, pretty basic stuff.
Problem I am having is the API and data is there, but it seems to be returning after the loop is running, is there any way of making the loop wait?
Heres the code;
Service (Hit the endpoint and retrieve the data)
'use strict';
function RecruiterDashJobs($resource, API_URL) {
var dashJobs = {};
dashJobs.getJobs = function(uuid) {
return $resource(API_URL + 'recruiters/' + uuid + '/jobs').get();
}
return dashJobs;
}
angular
.module('app')
.service('RecruiterDashJobs', RecruiterDashJobs);
Controller (Call the service and store the data)
$scope.currentRecruiter = User.getUser();
$scope.getJobs = function(uuid) {
var data = RecruiterDashJobs.getJobs(uuid);
data.$promise.then(
function(res) {
return res.jobs
},
function(err) {
return err;
}
)
};
$scope.recruiterJobs = $scope.getJobs($scope.currentRecruiter.uuid);
View (the Ng-repeat)
<div class="panel border-bottom pad-s-2x pad-e-1x" ng-repeat="job in recruiterJobs">
<div class="panel__body">
<aside class="valign">
<a class="icon--edit color--echo mar-r-2x" ui-sref="jobs/edit/{{job.uuid}"></a>
</aside>
<div class="valign">
<p>{{job.title}}</p>
<p class="color--charlie">Closing Date: {{job.closing_date}}</p>
</div>
</div>
</div>
EDIT: the "magical" approach below no longer works as of Angular 1.2
In your getJobs method, the return statements are inside child functions. You aren't returning the data from getJobs, you're returning it from the function you passed to then.
Two ways to fix it:
The magical Angular way for Angular less than 1.2
Angular views will work with promises for you, so you can just change your getJobs method to this:
$scope.getJobs = function(uuid) {
var data = RecruiterDashJobs.getJobs(uuid);
return data.$promise.then(
function(res) {
return res.jobs
},
function(err) {
return err;
}
)
};
Added return data.$promise...
If you want this to still work in Angular 1.2, you need to call $parseProvider.unwrapPromises(true) somewhere in your code, typically on your main modules config block.
Less magical way or for Angular 1.2 and above
If you want to better understand what is going on, then you can do it this way
$scope.getJobs = function(uuid) {
var data = RecruiterDashJobs.getJobs(uuid);
data.$promise.then(
function(res) {
$scope.recruiterJobs = res.jobs
},
function(err) {
console.log(err);
}
)
};
A $resource call is asynchronous, but $resource immediately returns an empty object that you can embed in your page and will be later populated by response contents. If all goes well, angular should spot the change (because it comes from a $resource process that angular monitors) and update your view accordingly.
So, the behaviour you observe is normal : the very premise of a $promise is that it will be done at a later stage and the process should proceed anyway.
Solutions :
Simply try :
$scope.getJobs = function(uuid) {
var data = RecruiterDashJobs.getJobs(uuid);
return data;
};
If you don't need to post-process data, this should be all you need (except that you might need to call recruiterJobs.jobs in your view, if your response does indeed return an object containing a jobs array, and not the array itself). The page will display, with an initial empty div, then update when data are retrieved and ng-repeat discovers new data to add to the page.
If you do need some post-processing, you can still use your callback :
$scope.getJobs = function(uuid) {
var data = RecruiterDashJobs.getJobs(uuid);
data.$promise.then(
function(res) {
//do something
},
function(err) {
return err;
}
);
return data;
};
If you really need to wait for your data (e.g. because there are some downstream processes that you need them for that can't be postponed), you can use the promise to do so :
$scope.getJobs = function(uuid) {
$scope.preparing = true;
var data = RecruiterDashJobs.getJobs(uuid);
data.$promise.then(function(res) {
$scope.preparing = false;
return data;
});
};
This way, the function will not return until the promise is resolved. I added an optional $scope.preparing flag that you can use in your page to inform the user that something is loading.

Not getting returned array of data from factory

I am making calls to two different firebase locations to get the data, once the data is $loaded, I am creating one object. then push this objects into one array and returning array.
But I am not getting array that is been return from factory
myApp.factory('MybetFactory', ['$firebase', function ($firebase) {
var factory = {};
factory.getUsersBetsProfile = function (userId, callback) {
//1. first call to firebase data
var firebaseUsersBetsProfileRef = new Firebase("https://x.firebaseio.com/profile").child(userId);
var userBettedEvent = [];
var userBetProfile = $firebase(firebaseUsersBetsProfileRef).$asObject();
//2. once object is loaded call second location and load the data
userBetProfile.$loaded().then(function () {
angular.forEach(userBetProfile, function (eachUserBetProfile) {
if (eachUserBetProfile !== null && eachUserBetProfile.hasOwnProperty('id')) {
var firebaseEventsResultsRef = new Firebase("https://x.firebaseio.com/result/+id");
var resultsForProfileBets = $firebase(firebaseEventsResultsRef).$asObject();
//3. once the data is loaded, push it into object created above
resultsForProfileBets.$loaded().then(function () {
console.log('Results for profile bets loaded', resultsForProfileBets);
eachUserBetProfile.results = resultsForProfileBets;
//4. push same object in array
userBettedEvent.push(eachUserBetProfile);
});
}
});
});
//Issue: this array is not been return in scope
return userBettedEvent;
};
return factory;
}]);
The reason that your array isn't visible on your return userBettedEvent line is because of how callbacks work in JavaScript.
Callbacks, which are things that look like this,
doSomethingCool("stuff", function() {...});
usually run once the function they're passed to completes. While your browser waits for doSomethingCool to finish, it goes on executing the subsequent code.
In other words, if you have code like this:
doSomethingCool("stuff", function() {
console.log("hello");
});
console.log("goodbye");
you're probably going to see this output:
goodbye
hello
To resolve your issue, you need to understand how callbacks work in JavaScript.

Trying to store and access JSON from within service

I'm relatively new to Angular and trying to correct some functionality someone left us with. I have a service that retrieves JSON from another service, and then I'd like to use that JSON within the original service. I have it working when accessing a single value from the JSON being returned, but I'd like to store the entire object so I can easily reference it throughout.
For instance, this works, and sets the value of title (title: cfgTitle) to the TITLE value coming back from the JSON:
.service("rtmModel", function($q, dataGetter, $http) {
this.getModel = function() {
var cfgProm = dataGetter.getData()
var cfgTitle = cfgProm.then(function(response) {
return response.data.record1.TITLE;
});
return {
title: cfgTitle,
desc:
...
However, if I return anything else from the "then" function I can't get stuff to show up. For instance, this does not work (appears to be passing undefined for the title param):
.service("rtmModel", function($q, dataGetter, $http) {
this.getModel = function() {
var cfgProm = dataGetter.getData()
var cfgTitle = cfgProm.then(function(response) {
return response.data.record1;
});
return {
title: cfgTitle.TITLE,
desc:
...
If I simply return "response" from the "then" function and do:
return {
title: cfgTitle.
then I get the entire JSON string passed into Title (and I understand why), but attempting to drill down (i.e. title: cfgTitle.data, or title: cfgTitle.data.record1) just results in undefined values.
I've tried various methods of storing the returning JSON data, but can't find the secret sauce that will allow me to store the JSON object, and then use it to pass multiple parameters down below (i.e. I want to pass a value into title, another into desc, and so forth).
Any pointers would be appreciated.
You can't return values/objects from the anonymous function callbacks that you pass to the .then method of a promise. Those callbacks are invoked internally by the promise library and the return value is ignored. You have two options. Refer a reference to a new object from getModel and copy the data returns from getData into it. Or, return a new promise from your getModel method, and resolve that promise when the promise returned from getData is resolved.
The later option (returning a promise to the caller) will look something like this (won't compile):
.service("rtmModel", function($q, dataGetter, $http) {
this.getModel = function() {
var defer = $q.defer();
dataGetter.getData().then(function(response) {
defer.resolve(response.data.record1);
});
return defer.promise;

Initialize service data with ajax request using promise?

I'm trying to create a service that will hold the shopping cart content of my website using AngularJS. I will then use this to make sure the reference to the cart in all controllers etc should be to the same object and synced.
The problem is that the cart content must be initialized via an ajax call. My code below does not work but it shows what I'm trying to accomplish. I would like to somehow get the list of items and return with getItems(), but if the list of items is not yet fetched then I will need to fetch for it first and promise a return. I'm trying to wrap my head around the "promise" concept but so far I have not fully got it yet.
factory('cartFactory', function ($rootScope, Restangular) {
var cart = $rootScope.cart = {};
return {
getItems: function () {
if (undefined == cart.items) {
return Restangular.oneUrl('my.api.cart.show', Routing.generate('en__RG__my.api.cart.show')).get().then(function($cart){
cart = $rootScope.cart = $cart;
angular.forEach(cart.items, function(value, key){
cart.items[key]['path'] = Routing.generate('en__RG__my.frontend.product.info', {'slug': value.variant.product.slug});
});
return cart.items;
});
} else {
return cart.items
}
},
setItems: function ($items) {
cart.items = $items;
},
removeItem: function ($item) {
cart.splice(cart.indexOf($item), 1);
},
addItem: function ($item) {
cart.items.push($item)
}
}
})
I will try to explain this in a very simplified way.
A promises is just an object that is "passed around" and we use this objects to attach functions that will be executed whenever we resolve, reject or notify the promise.
Because in Javascript objects are passed by reference we are able to refer to the same object in several places, in our case inside the service and the controller.
In our service we execute:
getItems: function () {
var deferred = $q.defer();
// do async stuff
return deferred.promise;
}
Lets say that the variable deferred above is an object more os less like this:
{
reject: function (reason) {
this.errorCallback(reason);
},
resolve: function (val) {
this.successCallback(val);
},
notify: function (value) {
this.notifyCallback(value);
},
promise: {
then: function (successCallback, errorCallback, notifyCallback) {
this. successCallback = successCallback;
this.errorCallback = errorCallback;
this.notifyCallback = notifyCallback;
}
}
}
So when we call getItems() a promise (deferred.promise) is returned and this allows the callee to set the callbacks to be executed whenever the promise changes its state (resolve, reject or notify).
So inside our controller I am setting only the resolve callback, if the promises is rejected it will happen silently because there is no errorCallback to be executed.
cartFactory.getItems().then(function (items) {
$scope.items = items;
});
Of course there is much more behind it, but I think this simplistic promise will help you get the basic idea. Be aware that cartFactory.getItems() must always return a promise, even when the items are already loaded, otherwise cartFactory.getItems().then() would break if , for example, you return an array.
Here a JSBin with your cartFactory service, I am using $timeout to simulate an async call.
Hope this helps.

Resources