$scope.authorizedUsers = [];
fbRef.child('folders/').child($routeParams.folderID).once('value',
function(ss) {
ss.forEach(function(childSnapshot) {
fbRef.child('users/').child(childSnapshot.name()).once('value',
function(user) {
$scope.authorizedUsers.push(user.val());
console.log($scope.authorizedUsers);
});
});
});
I use above code to look up master data then loop them in view using ng-repeat
<div ng-repeat="user in authorizedUsers">{{user}}</div>
but the looped data display sometimes only. Sometime means I have to refresh several time to get the data display.
Any idea or there is other better way to look up master data in firebase?
The callbacks are invoked asynchronously, so they don't happen within Angular's digest scope. Thus, they are not going to be applied to the page until a new compile occurs.
The solution that angularFire uses internally is to call $timeout(), which will force Angular's compile process to run. Some experimentation would reveal the most efficient way to get this done. Here's a brute force:
$scope.authorizedUsers = [];
fbRef.child('folders/').child($routeParams.folderID).once('value',
function(ss) {
ss.forEach(function(childSnapshot) {
fbRef.child('users/').child(childSnapshot.name()).once('value',
function(user) {
$timeout(function() {
$scope.authorizedUsers.push(user.val());
console.log($scope.authorizedUsers);
});
});
});
});
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
I'm trying to get my head around sharing data between multiple controllers, but couldn't find out yet how this is supposed to work (the angular way). I have create a Data service that look something like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
/// ... do async stuff
$rootScope.$broadcast("Data::filtered");
},
brush: function(brushed) {
/// ... do async stuff
$rootScope.$broadcast("Data::brushed");
},
load: function() {
/// ... do async stuff
$rootScope.$broadcast("Data::loaded");
}
};
});
Next I want to reuse and update data from this service, so I use it in my controller as follows:
angular.module('myapp.controllers')
.controller('FilterCtrl', function ($scope, $rootScope, DataSet) {
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
function updateBrushed() {
$scope.safeApply(function() {
$scope.brushed = DataSet.brushed;
});
};
$scope.brushed = [];
$scope.keepSelected = function() {
DataSet.filter(DataSet.FilterMethod.KEEP);
};
$scope.removeSelected = function() {
DataSet.filter(DataSet.FilterMethod.REMOVE);
};
$scope.$on('Data::brushed', updateBrushed);
$scope.$on('Data::filtered', updateBrushed);
});
The problem I have is basically illustrated by the use of the saveApply call. Basically I got this code from here: https://coderwall.com/p/ngisma. What I don't understand though is why I need it. As far as I can see, I'm 'within' $angular when updating the DataSet service. Nevertheless, the view for the Filter controller doesn't get updated without a call to saveApply ($apply doesn't work at all because than I run into the apply already in progress issue).
So, basically my question boils down to: is the approach above a good way to share data, and if so how is notification of changes in the service supposed to work?
Update: Based on Julian Hollman his suggestion I came to the following solution: http://jsfiddle.net/Ljfadvru/7/. This more or less illustrates the full workflow I was working on, though some of it is automatically induced in the fiddle, as opposed to user-interaction based in my real application. What I like about this approach is that it only sends signals when all data is updated.
Working with references, as suggested by Ed Hinchliffe, is nice as well. However, I'm working on a web visualization framework and I'm expecting tens of thousands of items. Clearing arrays and pushing new elements (which seem to me the consequence of this proposal) is really not feasible (if I understand this paradigm well, it would also result in a re-rendering of my vis for every single change). I stand corrected though if there are suggestions for further improvement.
$broadcast doesn't trigger an $apply and I bet your "async stuff" is not $http from angular.
So something happens outside of angular and angular doesn't know that something has changed.
In my opinion the best thing in that case is to write a wrapper for your async code and trigger $apply after date came back from the backend. Don't do it in the controller.
To be honest, I'm not sure quite sure about exactly what is going on with the digest loops in your particular scenario, but I don't think you are approaching this the right way.
The 'angular' way, is to use promises.
Your service should be more like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
var returnData = []
$http.get('/some/stuff').then(function(data){
for(i in data){
returnData.push(data[i]);
}
});
return returnData;
}
};
});
This sets up an empty placeholder object (returnData) that can be immediately passed to the controller, but a reference is kept so that when the data returns you can retrospectively populate that object. Because the controller and the service reference the same object, it'll 'just work'.
This way you don't have to worry about dealing with $digest or $apply or $broadcast.
You controller can just call $scope.filtered = DataSet.filter();
EDIT
If you want to be able to access the exact same data from multiple controllers:
angular.module('myapp.services')
.factory('DataSet', function($http) {
var cache = {
filtered: []
}
return {
getFiltered: function(){
if(cache.filtered.length) return cache.filtered;
$http.get('/some/url/').then(function(data){
for(i in data){
cache.filtered.push(data[i]);
}
});
}
};
});
I'm trying to apply the ngTable directive to my Rails app, but can't get it right. I'm still a newbe in angular and this directive seems very poorly documented (though has a great variety of nice examples).
So I have an array called $scope.users containing all the users info I need (e.g. like this), which comes from a $resource query().
And I need to turn it into a table with:
Sorting
Pagination
Filtering from one input (like done over here)
Can someone provide a solution or at least an advice how to put it all together?
UPDATE
So, with great help of wafflejock on the irc channel I've been able to put together some of the functionality. Here's the plunk.
What is still wrong:
The json dataset gets requested every time I change the page, the sorting or anything else. That's a huge amount of requests, so I need it to get cached somehow.
I need to be able to manipulate the dataset from the controller to change the values and remove users if need be. Still have no idea how to implement that.
Hi Almaron (aka Malkav) I'm wafflejock from the IRC here's the thing working as best I could get it:
http://plnkr.co/edit/TUOYmM?p=preview
var app = angular.module('main', ['ngTable']).
controller('DemoCtrl', function($scope, ngTableParams, NameService) {
var data = NameService.data;
$scope.tableParams = new ngTableParams(
{
page: 1, // show first page
count: 10, // count per page
sorting: {name:'asc'}
},
{
total: 0, // length of data
getData: function($defer, params) {
NameService.getData($defer,params,$scope.filter);
}
});
$scope.$watch("filter.$", function () {
$scope.tableParams.reload();
});
});
app.service("NameService", function($http, $filter){
function filterData(data, filter){
return $filter('filter')(data, filter)
}
function orderData(data, params){
return params.sorting() ? $filter('orderBy')(data, params.orderBy()) : filteredData;
}
function sliceData(data, params){
return data.slice((params.page() - 1) * params.count(), params.page() * params.count())
}
function transformData(data,filter,params){
return sliceData( orderData( filterData(data,filter), params ), params);
}
var service = {
cachedData:[],
getData:function($defer, params, filter){
if(service.cachedData.length>0){
console.log("using cached data")
var filteredData = filterData(service.cachedData,filter);
var transformedData = sliceData(orderData(filteredData,params),params);
params.total(filteredData.length)
$defer.resolve(transformedData);
}
else{
console.log("fetching data")
$http.get("http://www.json-generator.com/api/json/get/bUAZFEHxCG").success(function(resp)
{
angular.copy(resp,service.cachedData)
params.total(resp.length)
var filteredData = $filter('filter')(resp, filter);
var transformedData = transformData(resp,filter,params)
$defer.resolve(transformedData);
});
}
}
};
return service;
});
Basically I setup a few functions to do the those long lines to make it a bit easier to read and then setup a cachedData object that I check to see if it's populated before making the call... it looks like it still makes two calls very quickly at the beginning I'm sure you can avoid this by using a flag to check if the data is being loaded already and if so just have it wait instead of redoing the call but not sure it's a huge deal.
The second parameter of $http.get takes a config object - one of the keys it takes is cache, which you can set to true to cache your get request. Another method to reduce requests would be to use _.debounce from lodash or underscore to debounce requests made within a certain time period.
For editing data, in your template, you can have an input with ng-model and a span with a ng-bind (or double curly braces {{ }}) and have an ng-show and ng-hide on the same variable for them for editing.
Second call can be prevented by simple if condition scope.returnListGrid.settings().$scope!=null
I have arrays stored in Firebase, one of which I need to retrieve when a user logs in. Each user has their own array which requires authentication for read. (It would be inconvenient to switch to another data structure). Since $firebase() always returns an object, as per the docs, I'm using the orderByPriority filter. However, if I do simply
$scope.songs = $filter('orderByPriority')($firebase(myref));
that doesn't work as songs always get an empty array.
I don't understand why this happens, but what I've done to solve it is use the $firebase().$on('loaded',cb) form and applied the filter in the callback. Is this a good solution?
The drawback is that I cannot do $scope.songs.$save()
Here's my controller, including this solution:
.controller('songListController', function($scope, $rootScope, $firebase, $filter, $firebaseSimpleLogin){
var authRef = new Firebase('https://my-firebase.firebaseio.com/users'),
dataRef;
$scope.loginObj = $firebaseSimpleLogin(authRef);
$scope.songs = [];
$rootScope.$on("$firebaseSimpleLogin:login", function(event, user) {
// user authenticated with Firebase
dataRef = $firebase(authRef.child(user.id));
dataRef.$on('loaded', function(data){
$scope.songs = $filter('orderByPriority')(data);
});
});
//other controller methods go here
$scope.save = function(){
if (!$scope.loginObj.user)
{
alert('not logged in. login or join.');
return;
}
//Was hoping to do this
//$scope.songs.$save().then(function(error) {
//but having to do this instead:
dataRef.$set($scope.songs).then(function(error) {
if (error) {
alert('Data could not be saved.' + error);
} else {
alert('Data saved successfully.');
}
});
};
});
---Edit in response to Kato's answer---
This part of my app uses Firebase as a simple CRUD json store without any realtime aspects. I use $set to store changes, so I think I'm okay to use arrays. (I'm using jQueryUI's Sortable so that an HTML UL can be re-ordered with drag and drop, which seems to need an array).
I don't need realtime synchronisation with the server for this part of the app. I have a save button, which triggers the use of the $scope.save method above.
The problem with the approach above is that orderByPriority makes a single copy of the data. It's empty because $firebase hasn't finished retrieving results from the server yet.
If you were to wait for the loaded event, it would contain data:
var data = $firebase(myref);
data.$on('loaded', function() {
$scope.songs = $filter('orderByPriority')(data);
});
However, it's still not going to be synchronized. You'll need to watch for changes and update it after each change event (this happens automagically when you use orderByPriority as part of the DOM/view).
var data = $firebase(myref);
data.$on('change', function() {
$scope.songs = $filter('orderByPriority')(data);
});
Note that the 0.8 release will have a $asArray() which will work closer to what you want here. Additionally, you should avoid arrays most of the time.
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;
}