AngularFire - manage child_added and other events on $asArray - angularjs

Hello guys !
I wanted to do something pretty simple : display the last 10 posts with the newer at the top, and when someone posts a new one, display a bar "click here to see x new posts" that when clicked displays the new ones.
My problem : when a new posts enters Firebase, it immediately displays on screen (using the ng-repeat on the array of the scope linked to the array from Firebase), and takes the older of the 10 elements out.
$firebase(ref.limitToLast(10)).$asArray().$loaded().then(function(messagesData) { ... }
I can detect the change using
messagesData.$watch(function(data) {
console.log("data changed!", data, messagesData);
if(data.event == "child_added") {
// work here
}
});
But I can't figure out how to do what I'm trying to, nor did I find it in the doc. Thanks for any help !

Okay, so, there is a solution. It's possible not to have everything in sync while still enjoying the use of AngularFire for the current elements. Here is how.
Start by getting the messages you want to display at the beginning :
$firebase(ref.orderByKey().limitToLast(nbMessages)).$asArray().$loaded().then(function(messagesData) { ......
Then link them to another array one by one using foreach :
angular.forEach(messagesData, function(value, key) {
messagesDataReturn[key] = value;
// get any other data you need for that message (user name, etc...
});
This will keep every element iterated in sync. So if there is something like a counter of likes, they will be updated live.
Finally add a watcher :
messagesData.$watch(function(data) {
if(data.event == "child_added") {
var newLine = messagesData.filter(function ( obj ) {
return obj.$id === data.key;
})[0];
// add any needed data for this new line
$rootScope.$broadcast('list:updated', newLine);
}
});
In your controller you just have to listen to the broadcast :
$scope.newData = [];
$scope.$on('list:updated', function (event, data) {
$scope.newData.push(data);
});
You can then use $scope.newData to display the way to show the new messages and onclick merge this array with your main one on the scope, so the new messages appears.
Hope this helps someone !

Related

Mutating array within an array (Polymer iron-list)

I currently have an iron-list within another iron-list. The parent's data comes from a firebase-query element, and the child's data is computed from each parent item. The db structure and code looks a bit like this:
DB: [
category1: [
itemId1: {
price: 10,
title: "title"
}
]
]
<iron-list id="categoryList" items="{{categories}}" multi-selection as="category">
<template>
<div class="category-holder">
<iron-list id="{{category.$key}}" items="{{_removeExtraIndex(category)}}" as="item" selection-enabled multi-selection selected-items="{{selectedItems}}" grid>
<template>
<div class$="{{_computeItemClass(selected)}}">
<p>[[item.title]]</p>
<p>[[item.price]]</p>
</div>
</template>
</iron-list>
</div>
</template>
</iron-list>
After selecting any number of items, the user can tap on a fab to batch edit the price. This is where I'm having issues. I can't figure out how to access the correct child iron-list in order to call list.set...I'm currently trying the following very nasty method:
var categories = this.$.categoryList;
var categoryItems = categories.items;
(this.selectedItems).forEach(function(item) {
var index = item.itemId;
categoryItems.forEach(function(itemList, categoryIndex) {
if (itemList[index]) {
categories.set('item.' + categoryIndex + '.price', 10);
}
}, this);
}, this);
I'm iterating over the selected items in order to extract the item index and then iterating over the parent iron-list data (categoryItems) in order to check if the given item exists in that subset of data. If so, then I use the category index and attempt to call set on the parent iron-list using the given path to access the actual item I want to edit. As expected, this fails. Hopefully I've made myself clear enough, any help would be appreciated!
EDIT #1:
After much experimenting, I finally figured out how to correctly mutate the child iron-list:
(this.selectedItems).forEach(function(item) {
var list = this.$.categoryList.querySelector('#' + item.category);
var index = list.items.indexOf(item);
list.set(["items", index, "price"], 30);
}, this);
A couple of things worth noting. I'm using querySelector instead of the recommended this.$$(selector) because I keep running into a "function DNE" error. But now I have another problem...after calling the function, the value gets updated correctly but I get the following error:
Uncaught TypeError: inst.dispatchEvent is not a function
Here's a picture of the full error message:
I see the light, hopefully someone can help me out!
OK, I'll take a shot at this. I think the following happens, and I guess this based on how dom-repeat works:
var categories = this.$.categoryList;
var categoryItems = categories.items;
You take the variable that the iron-list is based on, but setting one array to another just creates a reference in javascript. As soon as you update categoryItems, you also update this.$.categoryList.items. When you later sets the new value, iron-list will do a dirty check and compare all subproperties, and because they are equal (because ... reference), the iron-list wont update the dom.
What you should do is to make sure it's a totally new copy and the way of doing that is to use JSON.parse(JSON.stringify(myArray)).
Further on, one major flaw I see in your code is that you're using querySelector to select an element, and then manipulate that. What you should do is to use this.categories and only that variable.
So your method should look something like:
// Get a freshly new array to manipulate
var category = JSON.parse(JSON.stringify(this.categories);
// Loop through it
category.forEach(category) {
// update your categoryList variable
}
// Update the iron list by notifying Polymer that categories has changed.
this.set('categories', category);

Adding views using for loop to scrollable view in appcelerator

Can anyone please tell me how to add an array of views using a for loop to a scrollable view in android. The answers that I surfed online are not giving me a clear idea and are way too confusing. Any suggestions?
You have (at least) two options. If you have a lot of work going on for each list item (e.g. showing a thumbnail for a picture) then you may want to use the second approach. Otherwise, you may just use this simple approach (where I show statistics for a number of species - layout controlled in a separate controller). stats is my list:
function showSpecies(stats) {
_.each(stats, function(record){
$.form.add(Alloy.createController('viewStatsRow', {record:record}).getView());
});
}
In this example I have more work going on for "building" every item. So to avoid locking the thread I use a list as a "queue" and just handles the first item - and then call the function with the remaining list until it is empty:
var work = [];
function showNextItem(work,first){
if(work && work.length > 0){
// Progressively show list....
if($.boastList && $.boastList.sections[0]){
var list = [];
list.push(buildOneItem(work.shift())); // Take first element
if(first){
$.boastList.sections[0].items = list; // Replace list
}else{
$.boastList.sections[0].appendItems(list); // append item
}
// Free queue to allow other actions
setTimeout(function(){
showNextItem(work); // Call recursively...
},30);
}
}else{
// All boasts shown...
}
}
function showBoastlist(){
work = [];
DataFactory.boasts.find({}, {$sort:{sortTime:-1}}, function(result){
result.forEach(function(record) {
work.push(record);
});
});
showNextItem(work,true);
}
The buildOneItem function just returns an item ready to be added to the view.
Not sure if this was what you asked for - but hope you can use it ;-)
Happy coding!
/John

how to retrieve data nested in two collections from firebase with angular

I'm new in Angular - Firebase development, and I am having problems to understand how to retrieve data nested in two collections.
I have a collection named "Orders", which includes a field call "auth", which is the user ID, and I have another collection that is the "User Profile", wich it's $id is the value of "auth". Inside the User Profile I have a field named roomNumber, and it's content I that I want to retrieve every time I read, in ng-repeat of the Orders.
In my view I was trying to do something like this :
<tr ng-repeat="item in items | filter: searchKeyword ">
<td align="left">{{item.$id}} - {{roomNumber(item.$id)}}</td></tr>
roomNumber is a function in my controller
$scope.roomNumber = function(id) {
var rootRef = new Firebase("https://xxxx-fire-yyyy.firebaseio.com/userProfile"+ '/' + id);
$scope.userdet = $firebaseArray(rootRef);
rootRef.on("value", function(rootSnapshot) {
var key = rootSnapshot.key();
var childKey = rootSnapshot.child("room").val();
console.log("room ", childKey)
});
return childKey
}
When I run this code and see results in my js console, strange things happend:
1. It repeat a lot of times
2. I can never get the childKey value
I have been reading Firebase documentation, but really I do not understand how to do this "silly" thing, does anybody give me a tip of how to do it?
When you bind a function to the $scope and call it within the html it expects to get an answer back right away when called. So when you query firebase and it takes its sweet time getting you back an answer, angularjs has already gotten an answer of undefined from the function.
So what is happening is that you are registering a callback when you provide the function to rootRef.on and then right after you register the callback you are returning the value of childKey. Unfortunately, childKey only gets set by the callback function (which firebase hasn't executed yet). Therefore angularjs gets an answer of undefined from your roomNumber function.
In order to make this work, you are going to have to get the room numbers beforehand and then probably add them to each of your items in $scope.items then use
<td align="left">{{item.$id}} - {{item.room}}</td></tr>
instead of
<td align="left">{{item.$id}} - {{roomNumber(item.$id)}}</td></tr>
To load all the room numbers you could call some function like this one after $scope.items has loaded
for (var i = 0; i < $scope.items.length; i++) {
var rootRef = new Firebase("https://xxxx-fire-yyyy.firebaseio.com/userProfile"+ '/' + $scope.items[i].$id);
$scope.userdet = $firebaseArray(rootRef);
rootRef.on("value", function(rootSnapshot) {
var key = rootSnapshot.key();
var childKey = rootSnapshot.val().room;
$scope.items[i].room = childKey;
});
}
It would change each of the items to have a reference to the room. Unfortunately, that list wouldn't update as the data updates, so the better solution would be to do that same query in whatever function was getting your items from the server and add the room to each item as it was being added to the items list.
To fix the issue with childKey not reading you need to use this:
var childKey = rootSnapshot.val().room;
instead of this:
var childKey = rootSnapshot.child("room").val();
console.log("room ", childKey)
Reference: https://www.firebase.com/docs/web/guide/retrieving-data.html

after angular $save and push to array item is not added unless I re-run a query

I have a single page app built using the MEAN stack. I am trying to get a list of items to update after a new item is added in angular but it's not working.
$scope.storages = Storages.query();
This works fine and returns my array of storages that I display in the view.
I then have a field to add a new array... to simplify the code it looks like this:
// create a new storage object from $scope.newStorage
var storage = new Storages($scope.newStorage);
I then do various things to the data and finally...
storage.$save(function(){
$scope.storages.push(storage); // doesn't seem to do anything
console.log("new storage", storage); //shows up fine
$scope.newStorage = []; // clear textbox
$scope.showHide.addItemPanel = false; // hides the newStorage form
return $scope.storages;
});
It seems like $scope.storages.push(storage) is not doing anything.
If I use this code at the end:
$scope.storages = Storages.query();
return $scope.storages;
Then it works. But I don't want to have to keep getting all the data from the server each time. How can I solve this and why isn't this working?
If I console.log(JSON.stringify($scope.storages)) it looks like this:
[{"_id":"XXXXXXXXXXXXXXX","_title":"XXXXXXXXX","__v":0,"files":[],"comments":[],"fields":[{"0":{"Title":"XXXXXXXXXXXXX"},"1":{"Category":"coding"},"2":{"Details":"XXXXXXXXX
Thanks in advance!
Read the comments below
storage.$save(function(){
//storage is promise here, not a Storage object you expect
$scope.storages.push(storage);
//because console.log binds to reference
//so after promise resolves, value on reference is changed
console.log("new storage", storage); //shows up fine
//not required at all
return $scope.storages;
});
Simple solution
//bind this to $scope
var storage = new Storages($scope.newStorage);
//like this
$scope.storage = new Storages($scope.newStorage);
//so Resource.$save is promise, when it completes, it will trigger digest
//and your array will be populated
$scope.storage.$save();
$scope.newStorage = [];
$scope.showHide.addItemPanel = false;
$scope.storages.push($scope.storage);
Better solution
//$save accepts callback
storage.$save(function(newStorageFromServer){
//newStorageFromServer is not promise, but actual result from server
//which is Storage instance by default
$scope.storages.push(newStorageFromServer);
//other staff
$scope.newStorage = []; // clear textbox
$scope.showHide.addItemPanel = false; // hides the newStorage form
//return statement is useless
});

AngularJS UI-calendar not updating events on Calendar

I am using Angular UI-Calendar to show some events on the Calendar. The events are showing fine on the Calendar. But when I update any event's details, the event's detail is actually modified, but not modified on the Calendar display(eg: start).
Initially, after I modified the event's details, I did a page reload to display modified changes and it worked too.In that method, I had empty $scope.events = []; array, which I filled after retrieving entries from DB.
But now, I want to avoid that page reload. So, once the event's details are modified from modal window, I clear the contents of $scope.events array using $scope.events = []; and then using API call, I fill the new events again in $scope.events array. This is working fine as the List view shows the modified events details. But the Calendar itself shows old entries. e.g if I change start from 11 April to 13 April, the calendar shows event on 11 April whereas List views shows the modified data i.e 13 April. Using Page reload, corrects this i.e event is shown on modified date(13 April).
How can I ensure that the event is modified on Calendar too without a Page reload ?
I tried calendar.fullCalendar('render'); after fetching new entries from DB, but it does not solve the Problem.
Here are some codes :
Initially I did this to send data to DB and then did a page reload to retrieve updated data.
$http.put(url,senddata).success(function(data){$window.location.reload();});
Now I want to avoid the page reload, so I did
$http.put(url,senddata).success(function(data){//$window.location.reload();//sends modified data to server
$scope.events = []; //clear events array
$http.get(url2).success(function(data){ //fetch new events from server, and push in array
$scope.schedule = data;
for(var i=0;i<data.length;i++)
{
$scope.events.push({
idx: data[i].idx,
title: data[i].title,
description : data[i].description,
allDay: false,
start: new Date(data[i].start),
end: new Date(data[i].end),
});
calendar.fullCalendar('render'); //Tried even this, didn't work
}
});
Above code pushes modified event in event array as visible in List view, but calendar still shows old event until page is reloaded.
Try maintaining the same array instance.
Instead of doing:
$scope.events = []
Try:
$scope.events.slice(0, $scope.events.length);
Then when your server request comes back, add each item individually to the existing array:
for(var i = 0; i < newEvents.length; ++i) {
$scope.events.push(newEvents[i]);
}
The reason I suggest this is because what you're describing sounds like the calendar might be holding onto the old list of events. The calendar might not know to update its reference, so instead, let it keep the same reference but change the contents of the array.
Just a quick correction to Studio4Development's answer. You should use "splice" not "slice". Slice returns the trimmed array. What we want to do is actually alter the original array. So you can use:
$scope.events.splice(0, $scope.events.length)
and to add new events:
$scope.events.push(newEvent)
Don't know if you found the solution to your problem, but what worked for me was:
calendar.fullCalendar('refetchEvents');
Here is how I fixed a similar problem on my page.
view (simplified, note using jade)
div#calendarNugget(ng-show="activeCalendar == 'Nugget'" ui-calendar="uiConfig.calendarNugget" ng-model="eventSources")
div#calendarWillow(ng-show="activeCalendar == 'Willow'" ui-calendar="uiConfig.calendarWillow" ng-model="eventSources2")
controller:
As per ui-calendar docs, I start with an empty array for my event sources. Ignore that I should probably rename these eventSources to something about the shop names (willow, nugget)
$scope.eventSources = [];
$scope.eventSources2 = [];
I then call a factory function that makes an http request and pulls a series of events from the DB. The events all have "title", "start", "end" properties (make sure your Date format is correct). They also have a "shop" property, which tells me which calendar to add the event to.
So after receiving the data I make two local arrays, loop through the received http data, and assign the events to those local arrays by shop. Finally, I can re-render the calendars with the proper event data by calling addEventSource, which automatically re-renders as per the fullCalendar docs
It looks something along the lines of this iirc:
function splitShiftsByShop(shifts) {
var nuggetShifts = [];
var willowShifts = [];
for (var i=0; i<shifts.length; i++) {
if (shifts[i].shop === "Nugget") {
var nshift = {
title : shifts[i].employee,
start : new Date(shifts[i].start),
end : new Date(shifts[i].end),
allDay: false
}
nuggetShifts.push(nshift);
} else if (shifts[i].shop === "Willow") {
var wshift = {
title : shifts[i].employee,
start : new Date(shifts[i].start),
end : new Date(shifts[i].end),
allDay: false
}
willowShifts.push(wshift);
}
}
/*render the calendars again*/
$('#calendarNugget').fullCalendar('addEventSource', nuggetShifts);
$('#calendarWillow').fullCalendar('addEventSource', willowShifts);
}
I was having some similar issues where events weren't being refetched after emptying my "eventSource" array ($scope.eventSources = [$scope.completedEvents]) and repopulating it with new events. I was able to overcome this at first by calling 'removeEvents',
uiCalendarConfig.calendars.calendar.fullcalendar('removeEvents')
This is hackish, so after further tinkering I found that events were refetched when my child array is modified,
$scope.completedEvents.splice(0, $scope.completedEvents.length)
After reviewing the source, I can see that the eventSource array is being watched, but the 'refetch' is never occurring. furthermore, I was never able to get the following to work,
uiCalendarConfig.calendars.calendar.fullcalendar('refetchEvents')
According to ui-calendar code, it does actually watch eventSources but never the actual individual sources. So doing a push to, let's say $scope.events ($scope.eventSources = [$scope.events]), will never trigger anything.
So push events to $scope.eventSources[0]
$scope.eventSources[0].splice(0, $scope.eventSources[0].length);
$scope.eventSources[0].push({
title : title,
start : start,
end : end
});
As you already know Full Calendar is dependant on JQuery. Therefore, all you need to do is put an ID on your calendar like this:
<div id="calendar" ui-calendar="uiConfig.calendar" class="span8 calendar" ng-model="eventSources"></div>
and then when ever you want to update the calendar just call:
$('#calendar').fullCalendar('refetchEvents')
I struggled with this for some time as well and this fixed all my problems. I hope this will help you too!
Goodluck!
Although it's late response but hope it may help some. I am putting it forth step by step.
You will need to maintain the same event source as Studio4Development mentioned earlier.
$scope.events //no need to reinitialize it
You will remove the updated event from events array like this (for last event in event array):
$scope.events.splice($scope.events.length - 1, 1);
For event that may exist anywhere in the events array you'll need to find its index using:
var eventIndex = $scope.events.map(function (x) { return x.Id; }).indexOf(eventId);
and then you'll remove it from events array like this:
$scope.events.splice(eventIndex, 1);
Next you'll fetch this event from the API again like this:
getEvent(data.Id);
and in the callback method you'll again add the event in events array:
var getSingleEventSuccessCallback = function (event) {
$scope.events.push(event);
};
Was stuck on this for a while.
What seems to be working for me turned out to be quite simple
I fetch my calendar from google and run an event formatter function. In this I define a new array and at the end of the function I set my eventSource equal to the new array. Then I call $scope.$apply
x = []
for e in events
x.push(
title: e.summary
etc...
)
$scope.eventSource = x
$scope.$apply()
Try this. It worked for me.
function flushEvents()
{
$scope.events.splice(0,$scope.events.length);
for(var i=0; i<$scope.events.length; i++)
{
$scope.events.splice(i,1);
}
}
I found this and it helped me:
$(id_your_div).fullCalendar('removeEvents');
$(id_your_div).fullCalendar('refetchEvents');

Resources