get nested object length in an angularFire array in Firebase - angularjs

I have a Firebase structure like this:
user {
uid {
Lessons {
lid1 {
Title: ...
}
lid2 {
Title: ...
}
}
}
}
I want to use AngularFire to convert user as array so I can filter them using Angular like this:
var usersRef = new Firebase($rootScope.baseUrl + "users");
var userListfb = $firebase(usersRef).$asArray();
The problem is, I also need the number of child of the Lessons object. When I log the userListfb, it is an array. But inside the array, the Lessons node still an object. I can not user length to get its length. What is the correct way to find out the number of child of the Lessons Node with Firebase AngularFire?
Edit 1
According to Frank solution, I got an infinite loop (digest circle error from AngularJS).
The problem is, I will not know the "uid" key. I need to loop it in the first array to get the uid into the second firebaseArray.
Let's say I have a ng-repeat="user in users" in the view and call this on view level in each repeat:
{{getLessonLength(user.uid)}}
Then in the controller, I have this function:
$scope.users = $firebaseArray($scope.usersRef);
$scope.getLessonLength = function (uid) {
var userRef = $rootScope.baseUrl + "users/" + uid + "/lessons/";
var lessonsNode = $firebaseArray(new Firebase(userRef));
return lessonsNode.length;
}
}
And it throw this error: Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []
All I want it is something like var lessonsCount = snapshot.child('lessons').numChildren() in regular Firebase .on('child_added' ...), the numChildren() function in FirebaseArray. Please help!

AngularFire contains quite some code to ensure that an ordered collection in your Firebase maps correctly to a JavaScript array as Angular (and you) expect it.
If you have a reference to a specific user, you can just create a new sync ($firebase) and call $asArray on that.
var usersRef = new Firebase($rootScope.baseUrl + "users");
var userListfb = $firebase(usersRef).$asArray();
var uid1LessonsRef = userRef.child('uid1').child('Lessons');
var uid1LessonsArray = $firebase(uid1LessonsRef).$asArray();
uid1LessonsArray.$loaded().then(function(arr) {
console.log('Loaded lessons, count: '+arr.length);
});
The data will only be synchronized once, no matter how many references you create to it.

Related

Mapping firebase data in ionic list

I am storing my data in firebase with update() like so
var newKey = firebase.database().ref().push().key;
var updates = {};
updates['metadata/' + newKey] = {
name: $scope.formData.name,
price: $scope.formData.price
};
updates['realdata/' + newKey] = {
name: $scope.formData.name,
price: $scope.formData.price,
date: $scope.formData.date
};
return firebase.database().ref().update(updates)
.then(function(ref){
console.log("added in fb");
}, function(error){
console.log("error " + error)
});
Now on an other page I am pulling the data out of firebase, but I can't seem to map it to my list in my view.
I tried multiple ways to pull the data out and in both ways I can see the data when logging it to the console.
var dbRef = firebase.database().ref('/metadata');
//Method 1
$scope.list = $firebaseArray(dbRef);
/*
Result here in the console is an array with objects
but when setting this in my list, I get the same amount of items pulled out but they are empty
*/
//Method 2 - I prefer this way as per docs it's a best practice
var loadmetadata = function(data){
console.log(data.val().name); // I get the actual name
$scope.list = data.val().name
};
dbRef.on('child_added', loadmetadata);
dbRef.on('child_changed', loadmetadata);
My view is just a simple
<ion-item ng-repeat="listitem in list">
{{ listItem.name }}
</ion-item>
What am I missing? I prefer the second method, if someone can help me achieve this?
The thing is I've found someone with the same problem here on SO, and he was able to solve it with the methods I have above. Here is the link to the question/answer Firebase 3 get list which contain generated keys to ionic list
The only difference I am seeing is that he's sorting the results, but I don't need that currently.
I've just figured it out! Instead of using $firebaseArray, I need to use the $firebseObject method.
$scope.metadataObj = $firebaseObject(dbRef);
and in my view I can do the following:
<ion-item ng-repeat="(key, value) in metadataObj">{{value.name}}</ion-item>
This method contains all the child methodes too. So no need to listen for them separetely.

Filter data by user with angularfire

I'm using angular-ui-fullcalendar to show and edit events. Users can log in and have unique uid when logged in. I want to use this to distinguish events made by current user from other events. I want to give current user events another backgroundColor.
What is the best way to do this??
I tried several things. My data looks like this:
```
database
bookings
-KWnAYjnYEAeErpvGg0-
end: "2016-11-16T12:00:00"
start: "2016-11-16T10:00:00"
stick: true
title: "Brugernavn Her"
uid: "1f17fc37-2a28-4c24-8526-3882f59849e9"
```
I tried to filter all data with current user uid like this
var ref = firebase.database().ref().child("bookings");
var query = ref.orderByChild("uid").equalTo(currentAuth.uid);
var bookings = $firebaseArray(query);
$scope.eventSources = [bookings];
This doesn't return anything. If I omit the filter in line 2 it returns all bookings as expected. But even if the filter worked it would not solve my problem, because I want to fetch both current user events and all other events. Firebase does not have a "not equal to" filter option...
I tried to loop through each record and compare uids and setting backgroundColor if condition was met:
var ref = firebase.database().ref().child("bookings");
var bookings = $firebaseArray(ref);
bookings.$ref().on("value", function(snapshot) {
var list = snapshot.val();
for (var obj in list) {
if ( !list.hasOwnProperty(obj) ) continue;
var b = list[obj];
if (b.uid === currentAuth.uid) {
b.className = "myBooking";
b.backgroundColor = "red";
}
}
});
$scope.eventSources = [bookings];
But this causes asynchronous problems so the 'bookings' array assigned to $scope.eventSources wasn't modified. I tried to move the $scope.eventSources = [bookings] inside the async code block but FullCalendar apparently can't handle that and renders nothing.
I also tried this but no luck either:
bookings.$loaded()
.then(function(data) {
$scope.eventSources = [data];
})
.catch(function(error) {
console.log("Error:", error);
});
What is the best solution to my problem?
If you're looking to modify the data that is loaded/synchronized from Firebase, you should extend the $firebaseArray service. Doing this through $loaded() is wrong, since that will only trigger for initial data.
See the AngularFire documentation on Extending $firebaseArray and Kato's answer on Joining data between paths based on id using AngularFire for examples.

Query from minimongo of large number of records stucks and hangs browser

I am building a page for admin in angular-meteor.
I have published all the records from a collection: "posts" and have taken the subscription of all the records on front end.
$meteor.subscribe('posts');
In the controller, if I select the cursors of all records from minimongo it works fine like:
$scope.posts = $meteor.collection(Posts);
But I want to display pagination, so for that I want limited records at a time like:
$scope.posts = $meteor.collection(function(){
return Posts.find(
{},
{
sort: {'cDate.timestamp': -1},
limit: 10
}
);
});
It stucks with the query in minimongo. And the browser hangs.
"posts" collection contains only 500 records. It was working fine when I had 200 records.
Can anyone give me an idea whats wrong with my code and concepts?
EDIT:
Okay! It worked fine when I commented the $sort line from query like this:
$scope.posts = $meteor.collection(function(){
return Posts.find(
{},
{
//sort: {'cDate.timestamp': -1},
limit: 10
}
);
});
But I need to sort the records. So what should I do now?
EDIT:
Also tried adding index to the sort attribute like this:
db.Posts.ensureIndex({"cDate.timestamp": 1})
Still same issue.
Change your publication to accept a parameter called pageNumber like this
Meteor.publish('posts', function (pageNumber) {
var numberOfRecordsPerPage = 10;
var skipRecords = numberOfRecordsPerPage * (pageNumber - 1);
return Post.find({
"user_id": user_id
}, {
sort: { 'cDate.timestamp': -1 }
skip: skipRecords,
limit: numberOfRecordsPerPage
});
});
On client side, I didn't work with angular-meteor much. You can create a pageNumber property under your current scope using this.pageNumber or $scope.pageNumber. Update this pageNumber variable whenever your pagination page is clicked. Whenever this variable is changed, subscribe using the current page number.
If it is using standard blaze template, I would do it using a reactive var or session var in an autorun like this.
In template html:
<template name="postsTemplate">
<ul>
<!-- you would want to do this list based on total number of records -->
<li class="pagination" data-value="1">1</li>
<li class="pagination" data-value="2">2</li>
<li class="pagination" data-value="3">3</li>
</ul>
</template>
In template js:
Template.postsTemplate.created = function () {
var template = this;
Session.setDefault('paginationPage', 1);
template.autorun(function () {
var pageNumber = Session.get('paginationPage');
Meteor.subscribe('posts', pageNumber);
});
}
Template.postsTemplate.events(function () {
'click .pagination': function (ev, template) {
var target = $(ev.target);
var pageNumber = target.attr('data-value');
Session.set('paginationPage', pageNumber);
}
});
This way, you will have a maximum of 10 records at any point in time on the client, so it will not crash the browser. You might also want to limit the fields that you send to client using something like this
Meteor.publish('posts', function (pageNumber) {
var numberOfRecordsPerPage = 10;
var skipRecords = numberOfRecordsPerPage * (pageNumber - 1);
return Post.find({
"user_id": user_id
}, {
sort: { 'cDate.timestamp': -1 }
skip: skipRecords,
limit: numberOfRecordsPerPage,
fields: {'message': 1, 'createdBy': 1, 'createdDate': 1 } //The properties inside each document of the posts collection.
});
});
And finally you will need the total number of records in posts collection on client side, to show the pagination links. You can do it using a different publication and using the observeChanges concept as mentioned in the official documentation here
// server: publish the current size of a collection
Meteor.publish("posts-count", function () {
var self = this;
var count = 0;
var initializing = true;
// observeChanges only returns after the initial `added` callbacks
// have run. Until then, we don't want to send a lot of
// `self.changed()` messages - hence tracking the
// `initializing` state.
var handle = Posts.find({}).observeChanges({
added: function (id) {
count++;
if (!initializing)
self.changed("postsCount", 1, {count: count});
},
removed: function (id) {
count--;
self.changed("postsCount", 1, {count: count});
}
// don't care about changed
});
// Instead, we'll send one `self.added()` message right after
// observeChanges has returned, and mark the subscription as
// ready.
initializing = false;
self.added("postsCount", 1, {count: count});
self.ready();
// Stop observing the cursor when client unsubs.
// Stopping a subscription automatically takes
// care of sending the client any removed messages.
self.onStop(function () {
handle.stop();
});
});
// client: declare collection to hold count object
PostsCount = new Mongo.Collection("postsCount");
// to get the total number of records and total number of pages
var doc = PostsCount.findOne(); //since we only publish one record with "d == 1", we don't need use query selectors
var count = 0, totalPages = 0;
if (doc) {
count = doc.count;
totalPages = Math.ceil(count / 10); //since page number cannot be floating point numbers..
}
Hope this helps.
Browser crashing because there is only so much data that it can load in it's cache before itself cashing out. To your question about what happens when you need to demand a large number of documents, take that process away from the client and do as much on the server through optimized publish and subscribe methods / calls. No real reason to load up a ton of documents in the browser cache, as minimongo's convenience is for small data sets and things that don't need to immediately sync (or ever sync) with the server.
you should sort on server side, you can find what you are looking for here
meteor publish with limit and sort
You need to consider sort and limit strategies:
Sort on the server if you're extracting top values from a large set used by all clients. But usually better first filter by User who needs the data, and sort on the filtered collection. That will reduce the dataset.
Then Publish that sorted / limited subset to the client, and you can do more fine grained / sorting filtering there.
You should use server side limit instead of client side. That will make your app faster and optimize.
For more please check this link.
link here

How to append a new value to an item within an array in Firebase?

Within Firebase, I have a list of 'ideas.' If a user presses a button associated with the idea, I'd like a value to be appended to that idea under an attribute called 'newValue.'
For example, the below html, uses ng-repeat to show the array of ideas and creates an associated button called 'Append Value.' I want a new value to be appended to the idea's attribute called 'newValue' every time a user presses 'Append Value.'
<body ng-controller="ctrl">
<table>
<tr class="item" ng-repeat="(id,item) in ideas">
<td>{{item.idea}}</td>
<td><input ng-model="newValue"></td>
<td><button ng-click="ValueAppend(id,newValue)">Append Value</button></td>
</tr>
</table>
</body>
Below is my attempt to create this function.
var app = angular.module("app", ["firebase"]);
app.factory("Ideas", ["$firebase", function($firebase) {
var Ref = new Firebase('https://crowdfluttr.firebaseio.com/');
var childRef = Ref.child('ideas');
return $firebase(childRef).$asArray();
}]);
app.controller("ctrl", ["$scope","Ideas", function($scope,Ideas) {
$scope.ideas = Ideas;
$scope.idea = "";
$scope.ValueAppend = function (id,newValue) {
var URL = "https://crowdfluttr.firebaseio.com/ideas/" + id + "newValue";
var IdeaRef = new Firebase(URL);
var IdeaData = $firebase(IdeaRef);
$scope.IdeaAttributes = IdeaData.$asArray();
$scope.IdeaAttributes.$add({
newValue: newValue,
timestamp: Date.now()
});
};
}]);
See my codepen for my working example: http://codepen.io/chriscruz/pen/PwZWKG
More Notes:
I understnad that AngularFire provides $add() and $save() to modify this array, but how could I use these methods so that I can add a new 'string' under an item in an array.
I'm not sure if these are your problems, but they are two typoes of mistakes in the code above and the codepen: typos and conceptual.
Typos
You forgot to inject $firebase into the controller, which leads to:
"ReferenceError: $firebase is not defined"
Solution is simply of course:
app.controller("ctrl", ["$scope","Ideas", "$firebase", function($scope,Ideas,$firebase) {
In addition you seem to be missing a slash before newValue, which means that you're trying to create a new idea instead of adding the value to an existing one. Solution is simple again, add a slash before newIdea as in:
var URL = "https://crowdfluttr.firebaseio.com/ideas/" + id + "/newValue";
If you find yourself making this mistake more often, you might be better server by the child function. Although it typically is a bit more code, it lends itself less to this typo of typo. Creating the ref to the newValue node becomes:
var URL = "https://crowdfluttr.firebaseio.com/ideas/";
var IdeaRef = new Firebase(URL).child(id).child("newValue");
Conceptual
With those trivial typos out of the way, we can focus on the real problem: which is easiest to see if you console.log the URL that you generate:
https://crowdfluttr.firebaseio.com/ideas/0/newValue
Yet if you look up the same data in the Firebase forge (by going to https://crowdfluttr.firebaseio.com/ideas/ in your browser), you'll see that the correct URL is:
https://crowdfluttr.firebaseio.com/ideas/-JbSSmv_rJufUKukdZ5c/newValue
That '0' that you're using comes from the id and it is the index of the idea in the AngularJS array. But it is not the key that Firebase uses for this idea. When AngularFire loads your data with $asArray it maps the Firebase keys to Angular indexes. We need to perform the reverse operation to write the new value to the idea: we need to map the array index (in id) back to the Firebase key. For that you can call [$keyAt(id)][1]. Since you keep the array of ideas in Ideas, it is simply:
var URL = "https://crowdfluttr.firebaseio.com/ideas/";
var IdeaRef = new Firebase(URL).child(Ideas.$keyAt(id)).child("newValue");
So the controller now becomes:
app.controller("ctrl", ["$scope","Ideas", function($scope,Ideas) {
$scope.ideas = Ideas;
$scope.idea = "";
$scope.ValueAppend = function (id,newValue) {
var URL = "https://crowdfluttr.firebaseio.com/ideas/";
var IdeaRef = new Firebase(URL).child(Ideas.$keyAt(id)).child("newValue");
var IdeaData = $firebase(IdeaRef);
$scope.IdeaAttributes = IdeaData.$asArray();
$scope.IdeaAttributes.$add({
newValue: newValue,
timestamp: Date.now()
});
};
}]);
I quickly gave it a spin in your codepen and this seems to work.

Accessing nested data with Angularfire

I am trying to capture changes to a nested array in my Firebase data. The data is laid out like so:
Calls
(Unique Firebase ID)
Station: "Foobar"
Time: "20:30"
Responders
(Unique Firebase ID)
Name: "Frank"
Avatar: "foo.jpg"
(Unique Firebase ID)
Name: "Martha"
Avatar: "bar.jpg"
I need to keep track of when calls are changed and when responders are added or removed. What I am struggling with is tracking the responders to a call - I will not have the random IDs ahead of time, so I need to track in real time as calls and responders to each call change.
Here is what I have so far. Some code related to paths and getting a JWT is missing-
var ref = new Firebase('https://foobar.firebaseio.com/clients/' + clientAuth);
var callRef = ref.child('calls');
$scope.activeCalls = 0;
$scope.calls = $firebase(callRef);
$scope.calls.$on("change", function() {
var keys = $scope.calls.$getIndex();
$scope.activeCalls = keys.length;
keys.forEach(function(key, i) {
console.log($scope.calls[key].responders);
// This provides an object with array of responders
});
});
UPDATE
I remembered child_added from the regular Firebase.. came up with this. A more eloquent way would be appreciated, I am trying to keep this more Angular based.
$scope.calls.$on("child_added", function(v) {
console.log(v.snapshot.value); // This gives me the details I need
});

Resources