I am writing a small Angular web application and have run into problems when it comes to loading the data. I am using Firebase as datasource and found the AngularFire project which sounded nice. However, I am having trouble controlling the way the data is being displayed.
At first I tried using the regular implicit synchronization by doing:
angularFire(ref, $scope, 'items');
It worked fine and all the data was displayed when I used the model $items in my view. However, when the data is arriving from the Firebase data source it is not formatted in a way that the view supports, so I need to do some additional structural changes to the data before it is displayed. Problem is, I won't know when the data has been fully loaded. I tried assigning a $watch to the $items, but it was called too early.
So, I moved on and tried to use the angularfireCollection instead:
$scope.items = angularFireCollection(new Firebase(url), optionalCallbackOnInitialLoad);
The documentation isn't quite clear what the "optionalCallbackOnInitialLoad" does and when it is called, but trying to access the first item in the $items collection will throw an error ("Uncaught TypeError: Cannot read property '0' of undefined").
I tried adding a button and in the button's click handler I logged the content of the first item in the $items, and it worked:
console.log($scope.items[0]);
There it was! The first object from my Firebase was displayed without any errors ... only problem is that I had to click a button to get there.
So, does anyone know how I can know when all the data has been loaded and then assign it to a $scope variable to be displayed in my view? Or is there another way?
My controller:
app.controller('MyController', ['$scope', 'angularFireCollection',
function MyController($scope, angularFireCollection) {
$scope.start = function()
{
var ref = new Firebase('https://url.firebaseio.com/days');
console.log("start");
console.log("before load?");
$scope.items = angularFireCollection(ref, function()
{
console.log("loaded?");
console.log($scope.items[0]); //undefined
});
console.log("start() out");
};
$scope.start();
//wait for changes
$scope.$watch('items', function() {
console.log("items watch");
console.log($scope.items[0]); //undefined
});
$scope.testData = function()
{
console.log($scope.items[0].properties); //not undefined
};
}
]);
My view:
<button ng-click="testData()">Is the data loaded yet?</button>
Thanks in advance!
So, does anyone know how I can know when all the data has been loaded
and then assign it to a $scope variable to be displayed in my view? Or
is there another way?
Remember that all Firebase calls are asynchronous. Many of your problems are occurring because you're trying to access elements that don't exist yet. The reason the button click worked for you is because you clicked the button (and accessed the elements) after they had been successfully loaded.
In the case of the optionalCallbackOnInitialLoad, this is a function that will be executed once the initial load of the angularFireCollection is finished. As the name implies, it's optional, meaning that you don't have to provide a callback function if you don't want to.
You can either use this and specify a function to be executed after it's loaded, or you can use $q promises or another promise library of your liking. I'm partial to kriskowal's Q myself. I'd suggest reading up a bit on asynchronous JavaScript so you get a deeper understanding of some of these issues.
Be wary that this:
$scope.items = angularFireCollection(ref, function()
{
console.log("loaded?");
console.log($scope.items[0]); //undefined
});
does correctly specify a callback function, but $scope.items doesn't get assigned until after you've ran the callback. So, it still won't exist.
If you just want to see when $scope.items has been loaded, you could try something like this:
$scope.$watch('items', function (items) {
console.log(items)
});
In my project I needed to know too when the data has been loaded. I used the following approach (implicit bindings):
$scope.auctionsDiscoveryPromise = angularFire(firebaseReference.getInstance() + "/auctionlist", $scope, 'auctionlist', []);
$scope.auctionsDiscoveryPromise.then(function() {
console.log("AuctionsDiscoverController auctionsDiscoveryPromise resolved");
$timeout(function() {
$scope.$broadcast("AUCTION_INIT");
}, 500);
}, function() {
console.error("AuctionsDiscoverController auctionsDiscoveryPromise rejected");
});
When the $scope.auctionsDiscoveryPromise promise has been resolved I'm broadcasting an event AUCTION_INIT which is being listened in my directives. I use a short timeout just in case some services or directives haven't been initialized yet.
I'm using this if it would help anyone:
function getAll(items) {
var deferred = $q.defer();
var dataRef = new Firebase(baseUrl + items);
var returnData = angularFireCollection(dataRef, function(data){
deferred.resolve(data.val());
});
return deferred.promise;
}
Related
I'm using FireBase and trying to do some queries, the results are logging in but are not visible in the HTML $scope.
var shopRef = firebaseDataService.intro;
$scope.shops = [];
var taskRef = shopRef.orderByChild("cat").equalTo("Accomodation");
taskRef.on("value", function(snapshot) {
var snapData = snapshot.val();
console.log(snapData);
$scope.shops.push(snapData);
});
When I use $scope.$apply(), I manage to get the data updated to shops, but it's still not passing anything to my directive .
<search-card shops="shops"> </search-card>
<p> Shops are {{shops}}</p>
I got it working somehow with $firebaseArray
$scope.shops = $firebaseArray(taskRef);
but I`d still like to know what I'm doing wrong and why it's not working with the snapshot.
From the angularfire docs:
// read data from the database into a local scope variable
ref.on("value", function(snapshot) {
// Since this event will occur outside Angular's $apply scope, we need to notify Angular
// each time there is an update. This can be done using $scope.$apply or $timeout. We
// prefer to use $timeout as it a) does not throw errors and b) ensures all levels of the
// scope hierarchy are refreshed (necessary for some directives to see the changes)
$timeout(function() {
$scope.data = snapshot.val();
});
});
It seems that using $scope.apply() will not refresh the entire hierarchy (and hence the directive). Try using $timeout as prescribed instead
That being said, I think you should go with the $firebaseArray() option as that strikes me as the most "angular" solution
Im working on angularjs 1.4. Im trying to have some frontend-cache collection that updates the view when new data is inserted. I have checked other answers from here Angularjs watch service object but I believe Im not overwriting the array, meaning that the reference is the same.
The code is quite simple:
(function(){
var appCtrl = function($scope, $timeout, SessionSvc){
$scope.sessions = {};
$scope.sessions.list = SessionSvc._cache;
// Simulate putting data asynchronously
setTimeout(function(){
console.log('something more triggered');
SessionSvc._cache.push({domain: "something more"});
}, 2000);
// Watch when service has been updated
$scope.$watch(function(){
console.log('Watching...');
return SessionSvc._cache;
}, function(){
console.log('Modified');
}, true);
};
var SessionSvc = function(){
this._cache = [{domain: 'something'}];
};
angular.module('AppModule', [])
.service('SessionSvc', SessionSvc)
.controller('appCtrl', appCtrl);
})();
I thought that the dirty checking would have to catch the changes without using any watcher. Still I put the watcher to check if anything gets executed once the setTimeout function is triggered. I just dont see that the change is detected.
Here is the jsbin. Im really not understanding sth or doing a really rockie mistake.
You need to put $scope.$apply(); at the bottom of your timeout to trigger an update. Alternatively you can use the injectable $timeout service instead of setTimeout and $apply will automatically get called.
jsbin
I have some parameters in the $rootScope as specified below:
myApp.factory('itemService', function($http) {
return $http.get('/items');
});
myApp.run(function($rootScope, itemService) {
itemService.success(function(response) {
$rootScope.items = response;
});
});
myApp.controller('displayCtrl', function($rootScope, $scope) {
$scope.items = $rootScope.items;
});
When I run the above code, I get this error from firebug
TypeError: $rootScope.items is undefined. I really do not know what is happening.
Here is a small addition. items is an array with a list of objects like this:
items = [
{'name': 'spoon', 'price': 200},
{'name': 'table', 'price': 400},
{'name': 'shoe', 'price': 250}
];
I wish to make items available constantly in my app such that I can display each item on the item list (items) without making another request to the server. I intend to achieve this by simply displaying an item using $scope.item = items[$routeParams.id] each time I need to display an item.
I look forward to implement this using either a function attached to ng-click or the normal #/route/:param mechanism.
Thanks
TypeError: $object.property is undefined is usually because a request to a reference of an object is made before that specific object (or its property) has been set. $http requests are asynchroneous by nature so other processes do not get blocked. It should be obvious that trying to make requests synchroneous could cause a major issue for people with very slow connections.
Apart from that, polluting the $rootScope is generally a bad idea. You can find a topic about global variables on the following link so that you investigate why the $rootScope is not such a good place.
Having said all that, it seems to me that you didn't want to make multiple requests to retrieve the same data. If so, you can use the cache option for $http.get methods.
e.g:
myApp.factory('itemService', function($http, $q) {
return {
get: function() {
return $http({
url: 'items.json',
cache: true //keep the result in memory
});
}
};
})
myApp.controller('aCtrl', function(itemService) {
var self = this;
itemService.get().success(function(data) {
self.items = data;
});
});
myApp.controller('bCtrl', function(itemService) {
var self = this;
itemService.get().success(function(data) {
self.items = data;
});
});
This will make sure the information gets requested once and put into a cache. The data is accessible in different places.
<div ng-controller="aCtrl as a">
{{a.items}}
</div>
<div ng-controller="bCtrl as b">
{{b.items}}
</div>
This leaves me with another 'good' practice: the usage of the controllerAs syntax. Which provides a way to use namespaces in AngularJS.
Ofcourse, these are just tips and you should always consider the requirements!
You run asynchronious method at run block :
itemService.success(function(response){
$rootScope.items = response;
});
But initialization goes on, so probably you access $rootScope.items before itemService succeed (or it fails, and you didnt predict such situation). I suggest you to do this (if you want to follow $rootScope convension.. which is bad by the way) :
$rootScope.items = [];
itemService.success(function(response){
$rootScope.items = response;
});
You are setting items in the callback of an asynchronous process, so you are trying to access items on the $rootScope before its actually set.
If you are trying to initialize items when the controller is loaded, then there are other ways to do that such as using the resolve block of a route or manually calling the $http.get on the factory when the controller loads.
Finally, I was able to come up with a solution. I realized that the problem was to have $rootScope.items available in displayCtrl at the same time it loads. But $rootScope.items is available in my view when my html page loads.
So I simply passed the item id as a parameter and obtained it using $routeParams as follows
myApp.controller('displayCtrl', function($routeParams, $scope) {
$scope.item_id = $routeParams.id; //given that the route looks like '/item/:id'
});
Then in my HTML file this what I did
<div ng-bind="items[item_id].name"></div>
<div ng-bind="items[item_id].price"></div>
This actual solved my problem.
I have a method in a controller which executes the following code:
this.StockService.GetByInvoicesID(this.SelectedInvoice.ID).success((StockItems) =>
{
this.StockItems = StockItems;
this.CreditNoteStockItems = new Array<ViewModels.CreditNoteStockItemViewModel>();
}
Before this service method is called, all members in the controller are defined. However, once the promise resolves, this.StockItems and this.CreditNoteStockItems are all undefined. Furthermore, the assignment of StockItems is not being reflected in the view. I'm guessing this is a scope issue and the promise is returning into a new scope. This has happened with other methods before, it almost seems to happen at random.
My questions are, can anyone tell me why exactly this is happening, and more importantly, how can I prevent it from occurring?
edit: This is a simplified version of my controller (all thats missing is several members and methods)
edit 2: more info about method in controller
export class CreditNoteController
{
static $inject = ['$scope', '$modalInstance', 'StockService'];
StockService: Services.StockService;
ModalInstance: ng.ui.bootstrap.IModalServiceInstance;
constructor($scope, $modalInstance, StockService: Services.StockService)
{
$scope.vm = this;
this.ModalInstance = $modalInstance;
this.StockService = StockService;
}
InvoicesSelectionChanged()
{
this.StockService.GetByInvoicesID(this.SelectedInvoice.ID).success((StockItems) =>
{
this.StockItems = StockItems;
this.CreditNoteStockItems = new Array<ViewModels.CreditNoteStockItemViewModel>();
});
}
}
The controller is injected through the angular UI modal service open method sitting in another controller:
this.ModalService.open(
{
templateUrl: URL,
controller: Controllers.CreditNoteController,
});
edit 2: The javascript that it generates
CreditNoteModalController.prototype.InvoicesSelectionChanged = function () {
var _this = this;
this.StockService.GetByInvoicesID(this.SelectedInvoice.ID).success(function (StockItems) {
_this.StockItems = StockItems;
_this.CreditNoteStockItems = new Array();
});
};
Thanks
After some back and forth in the comment thread, turns out there were 2 things going on here, so consolidating the info into the answer:
When inspecting 'this' in a debugger in a .ts file, know that you will be seeing the actual 'this' value and not the _this alias that is created in the .js file when you use arrow functions. This can sometimes make it look like variables aren't getting set when in reality, you are inspecting the wrong variable. Setting a watch on _this will show the right one. You also have the option of using console.log on 'this' in the TypeScript code itself, since it will compile to using the _this alias.
ng-options won't work without ng-model
I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>