Load the last 3 and every new child - javascript

What would be the best way to get the 3 last messages and append/push all new messages into a array? Would the following be ok? What if I would like to use $firebaseArray ?
var messages = [];
ref.child('messages').limitToLast(3).on('child_added', function(snap){
messages.push(snap.val());
});
var tempMessages = $firebaseArray(ref.child('messages').limitToLast(3));
messages.$loaded(function(data){
messages = data;
})

What you secretly want is a range query.
You want to start at the last three and listen onwards. You can use the .start() function to create a range query. Once you know where to start the range you can start at the last 3 messages and get every one after that.
Here's a JSBin demo of the last three messages, plus any newly added messages.
The problem is though you have to know what the 3rd to last key is first.
To do that we'll do an initial query of the last three, get the 3rd to last key and then run our range query. This is best done with a .once function, because we only care to get the last three items one time.
function LimitMessagesArray(Ref, $q) {
// return a function so we can alter the limit amount
return function LimitMessagesArray(lastLimit) {
var limitQuery = Ref.child('messages').limitToLast(lastLimit);
// Since it's a one time read we'll use a promise
var deferred = $q.defer();
// Reading only once is necessary
limitQuery.once('value', function(snap) {
var newMessages = [];
// iterate through the snapshot and keep the key around
snap.forEach(function(childSnap) {
var newMessage = childSnap.val();
newMessage.$key = childSnap.key();
newMessages.push(newMessage);
});
// resolve the last 'N' messages
deferred.resolve(newMessages);
});
return deferred.promise;
};
}
Now we have the last 'N' messages in an promise-fulfilled array. We want to take the first key of the array and use that to start the range query. To make this easier we'll create a factory a range of messages.
function StartAtMessagesArray(Ref, $firebaseArray) {
return function StartAtMessagesArray(startingKey) {
var rangeQuery = Ref.child('messages').orderByKey().startAt(startingKey);
return $firebaseArray(rangeQuery);
};
}
Once we have these two pieces we can use them together to listen to the last three messages plus all of the newly added ones.
function MyController($scope, limitMessages, startAtMessages, Ref) {
var lastThreeMessages = limitMessages(3);
// When the last three messages load get the first key
lastThreeMessages.then(function(data) {
var startingKey = data[0].$key;
// Create the sync array of messages from the starting key
$scope.messages = startAtMessages(startingKey);
});
}

Inject firebaseArray() to your controller and define the reference you want the array for. For example:
app.controller("Ctrl", ["$scope", "$firebaseArray",
function($scope, $firebaseArray) {
var desiredRef = new Firebase("URL of your app ");
Now create an array for data for firebaseArray(desiredRef) -
$scope.tempMessages = $firebaseArray(desiredRef);
Through the same firebaseArray() service you can use query() for last three messages -
var query = desiredRef.orderByChild("timestamp").limitToLast(3);
//create another array for last(3)
$scope.lastThree = firebaseArray(query);
Official documentation quotes:
This is a PSEUDO READ-ONLY ARRAY suitable for use in directives like
ng-repeat and with Angular filters (which expect an array).
Another note, with this service push(), splice() and other functions will make the array unable to be updated. So use specific APIs as suggested in the documentation. For instance, $add instead of push().
Further reference and example here.

Related

Javascript: Values of array disappear after referring to it from other function

I currently try to build a form with javascript which has two functionalities:
1) Adding elements dynamically to a list
2) Identify through a button click a certain element (e.g. with the highest value)
See (wanted to add pictures directly to my post, but I am lacking StackOverflow reputation - so here are they as links):
https://i.ibb.co/KxvV5Ph/Bildschirmfoto-2019-11-03-um-19-12-51.png
First functionality works fine (see above, added installations). The second doesnt. My plan was the following:
1) When an element gets added to the list I also push it as an object of the class "installation" to the array installations = []
2) When I click on "Identify Longest Duration" I iterate through a map function over the installations array (and output the highest value as an alert).
Unfortunately, the installations array is empty when I call it from another function.
Get values from form
var instStorage = document.getElementById("instStorage");
var instMasse = document.getElementById("instMasse");
var instPrice = document.getElementById("instPrice");
var instDischarge = document.getElementById("instDischarge");
const installations = [] ; // empty installations array
Adding values to DOM, Calling another function to add values to installations array
const createInstallation = () => {
... (working code to add vars from 1) to list element in DOM)...
addInstallation(); // calling another function to add installation to installations array
}
Class Definition of installation
class installation {
constructor(storage, masse, price, discharge) {
this.storage = storage;
this.masse = masse;
this.price = price;
this.discharge = discharge;
}
... (getter functions here) ...
summary = () => {
return `Installation Specs: Storage ${this.getStorage()},
Mass ${this.getMasse()}, Price ${this.getPrice()} and Discharge
Rate
${this.getDischarge()}`;
}
}
Adding installation to installations array
const addInstallation = () => {
installations.push(new installation(instStorage, instMasse, instPrice, instDischarge));
(...)
}
When I call for test purposes my summary function within the createInstallation() function (after calling addInstallation()) everything works fine; the first element of the installations array gets displayed:
alert(installations[0].summary());
See:
https://i.ibb.co/Khc6R7r/Bildschirmfoto-2019-11-03-um-19-32-41.png
When I call the summary function from the event listener of the "Identify Longest" button the installations array is suddenly empty, even though I added an element a second before (which should have been added to the installations array).
See:
https://i.ibb.co/80bTFWY/Bildschirmfoto-2019-11-03-um-19-36-48.png
I am afraid that's a problem of scope; but I don't see how to fix it.
Help is appreciated :-)
Thanks a lot!
Get values from form
var instStorage = document.getElementById("instStorage");
That is what is not happening. getElementById() gets you an element, by id. Not its value.
Assuming that instStorage and friends are input elements (which is not shown unfortunately), you may want to change the code to actually get their value:
var instStorage = document.getElementById("instStorage").value;
var instMasse = document.getElementById("instMasse").value;
var instPrice = document.getElementById("instPrice").value;
var instDischarge = document.getElementById("instDischarge").value;

Query multiple keys while listening to changes - Firebase

I am using this question as a reference, since it is very similar.
If I have a set of defined keys:
-Ke1uhoT3gpHR_VsehIv
-Ke8qAECkZC9ygGW3dEJ
-Ke8qMU7OEfUnuXSlhhl
and also know that they all do exist under a node (let's say /items), how would I query for them, while also listening for changes on those keys (aka not using myRef.once(value) but using myRef.on('value', snapshot => { ... })
I know this is possible:
var keys = [
"-Ke1uhoT3gpHR_VsehIv",
"-Ke8qAECkZC9ygGW3dEJ",
"-Ke8qMU7OEfUnuXSlhhl"
];
var promises = keys.map(function(key) {
return firebase.database().ref("/items/").child(key).once("value");
});
Promise.all(promises).then(function(snapshots) {
snapshots.forEach(function(snapshot) {
console.log(snapshot.key+": "+snapshot.val());
});
});
but it is a static snapshot. Could it be possible to do the same while listening for changes?
I also have been looking for this. So it seems it doesn't exist, simply because Firebase has an efficient connection it is not necessary to combine 'queries' for the sake of connection performance.
So you would end up setting on('value', ...) event listeners for all keys. The code you could write for that could look like this, based on the code fragment given in your question:
var keys = [
"-Ke1uhoT3gpHR_VsehIv",
"-Ke8qAECkZC9ygGW3dEJ",
"-Ke8qMU7OEfUnuXSlhhl"
];
function itemUpdate(snapshot) {
console.log(snapshot.key+": ", snapshot.val());
}
var ref = firebase.database().ref("/items/");
keys.forEach(function(key) {
ref.child(key).on("value", itemUpdate);
});
Where the only improvements would be to define the callback function or else you would create as many callback functions as you have keys to listen on and second to place the ref in a var.

How can I control when the component's createContent function is fired?

I have an app which needs to check if the user has multiple accounts when it is first loaded. Most of them don't, so they should be taken straight into the main app view.
If they do have multiple accounts, they should be taken to a screen which allows them to select with which account they want to continue. The users with only one account should never see this selection page.
I have an OData call which returns the result of a backend check to see if the user has multiple accounts.
How can I dynamically set the rootView property of the component's metadata so it acts according to the behaviour defined above?
My research suggests if the rootView property is not set in the component metadata, the root view must be defined in the createContent() method.
My problem seems to stem around the asynchronous call to the OData, with createContent() executing before the OData result functions have set the variable to decide which view to see.
EDIT: I've changed the title to better reflect the likely cause of my problem. How can I keep the asynchronous calls but prevent createContent() from eventing before those calls are completed? Adding in something like the EventBus still doesn't work as it seems the application won't wait for it, and I just get an error about not being able to find the view.
My code below:
Declaration at the top of the file. By default view is the App since most users don't have multiple accounts. The multiple accounts is a rarer exception.
var sRootView = "view.App"; //default value of root view
sap.ui.core.UIComponent.extend("MY_APP.Component", {
Component.js
init: function(){
...
this.oModel = new sap.ui.model.odata.ODataModel("/odatasvc");
that = this;
var aBatch = [];
aBatch.push(this.oModel.createBatchOperation("/multiple_account_check", "GET"));
makeCall = function(){
var oDef = $.Deferred();
utils.oDataCalls.read(that.oModel, aBatch, oDef);
return oDef.promise();
};
var promise = makeCall();
var aResults = [];
promise.done(function(data) {
for (var i=0; i<data.length; i++) {
aResults.push(data);
}
that.checkAccounts(aResults[0]);
});
The createContent function.
createContent : function(){
var oView = sap.ui.view({
id : "meViewId",
viewName : sRootView,
type : "XML" });
return oView;
},
The function to check OData result for multiple accounts.
checkAccounts : function(aResults){
if(aResults.length == 1){ //one account
that.oRouter.navTo("home");
}
else if(aResults.length > 1){ //multiple accounts
sRootView = "view.multipleAccountsView"; //changed value used in createContent
that.oRouter.navTo("multipleAccounts");
}
}
You should be able to make the OData call synchronously by passing async as false to ODataModel method read. E.g:
oModel.read{"path", {async: false}};
If you implement this inside utils.oDataCalls.read, it might also require reworking the promise in that function.

Extracting values from USGS real time water service

There must be something simple I am missing, but alas, I do not know what I do not know. Below is the code I have thus far for trying to get current streamflow conditions from the USGS.
// create site object
function Site(siteCode) {
this.timeSeriesList = [];
this.siteCode = siteCode;
this.downloadData = downloadData;
this.getCfs = getCfs;
// create reference to the local object for use inside the jquery ajax function below
var self = this;
// create timeSeries object
function TimeSeries(siteCode, variableCode) {
this.variableCode = variableCode;
this.observations = [];
}
// create observation object
function TimeSeriesObservation(stage, timeDate) {
this.stage = stage;
this.timeDate = timeDate;
}
// include the capability to download data automatically
function downloadData() {
// construct the url to get data
// TODO: include the capability to change the date range, currently one week (P1W)
var url = "http://waterservices.usgs.gov/nwis/iv/?format=json&sites=" + this.siteCode + "&period=P1W&parameterCd=00060,00065"
// use jquery getJSON to download the data
$.getJSON(url, function (data) {
// timeSeries is a two item list, one for cfs and the other for feet
// iterate these and create an object for each
$(data.value.timeSeries).each(function () {
// create a timeSeries object
var thisTimeSeries = new TimeSeries(
self.siteCode,
// get the variable code, 65 for ft and 60 for cfs
this.variable.variableCode[0].value
);
// for every observation of the type at this site
$(this.values[0].value).each(function () {
// add the observation to the list
thisTimeSeries.observations.push(new TimeSeriesObservation(
// observation stage or level
this.value,
// observation time
this.dateTime
));
});
// add the timeSeries instance to the object list
self.timeSeriesList.push(thisTimeSeries);
});
});
}
// return serialized array of cfs stage values
function getCfs() {
// iterate timeseries objects
$(self.timeSeriesList).each(function () {
// if the variable code is 00060 - cfs
if (this.variableCode === '00060') {
// return serialized array of stages
return JSON.stringify(this.observations);
}
});
}
}
When I simply access the object directly using the command line, I can access individual observations using:
> var watauga = new Site('03479000')
> watauga.downloadData()
> watauga.timeSeriesList[0].observations[0]
I can even access all the reported values with the timestamps using:
> JSON.stringify(watauga.timeSeriesList[0].observations)
Now I am trying to wrap this logic into the getCfs function, with little success. What am I missing?
I don't see anything in the code above that enforces the data being downloaded. Maybe in whatever execution path you're using to call getCfs() you have a wait or a loop that checks for the download to complete prior to calling getCfs(), but if you're simply calling
site.downloadData();
site.getCfs()
you're almost certainly not finished loading when you call site.getCfs().
You'd need to do invoke a callback from within your success handler to notify the caller that the data is downloaded. For example, change the signature of Site.downloadData to
function downloadData(downloadCallback) {
// ...
Add a call to the downloadCallback after you're finished processing the data:
// After the `each` that populates 'thisTimeSeries', but before you exit
// the 'success' handler
if (typeof downloadCallback === 'function') {
downloadCallback();
}
And then your invocation would be something like:
var watauga = new Site('03479000');
var downloadCallback = function() {
watauga.timeSeriesList[0].observations[0];
};
watauga.downloadData(downloadCallback);
That way, you're guaranteed that the data is finished processing before you attempt to access it.
If you're getting an undefined in some other part of your code, of course, then there may be something else wrong. Throw a debugger on it and step through the execution. Just bear in mind that interactive debugging has many of the same problems as interactively calling the script; the script has time to complete its download in the background before you start inspecting the variables, which makes it look like everything's hunky dory, when in fact a non-interactive execution would have different timing.
The real issue, I discovered through just starting over from scratch on this function, is something wrong with my implementation of jQuery.().each(). My second stab at the issue, I successfully used a standard for in loop. Here is the working code.
function getCfs() {
for (var index in this.timeSeriesList) {
if (this.timeSeriesList[index].variableCode === '00060'){
return JSON.stringify(this.timeSeriesList[index].observations);
}
}
}
Also, some of the stuff you are talking about #Palpatim, I definitely will have to look into. Thank you for pointing out these considerations. This looks like a good time to further investigate these promises things.

AngularFire Collection Length

I'm attempting to get the length of the array of a simple angularFireCollection and can't seem to:
var stf = new FireBase("http://myfirebase-app.firebaseio.com/staff");
function staffListCtrl($scope, angularFireCollection){
$scope.staff = angularFireCollection(stf);
console.log($scope.staff.length);
}
The output in the console says:
0
Which I know is incorrect. It should be returning somewhere around 5 as the length (see screenshot below for the output of $scope.staff.
Any help is a appreciated as I can't seem to get past this absolutely, utterly simple JS task.
In this case, the correct way to print the number of retrieved elements within the callback would be following:
var stf = new FireBase("http://myfirebase-app.firebaseio.com/staff");
function staffListCtrl($scope, angularFireCollection) {
$scope.staff = angularFireCollection(stf, function() {
console.log(stf.numChildren());
});
}
The reason for this is that the initial callback function is called before actual assignment to $scope.staff takes place. So within the callback function you can access only the Firebase DataSnapshot object stf. Hope this helps.
You're trying to access the length immediately after calling angularFireCollection, but the actual data is retrieved over the network and therefore it takes a little while for the array to be updated. You can pass a function as an argument to angularFireCollection to be notified about when the initial data is loaded, like so:
var stf = new FireBase("http://myfirebase-app.firebaseio.com/staff");
function staffListCtrl($scope, angularFireCollection) {
$scope.staff = angularFireCollection(stf, function() {
console.log($scope.staff.length);
});
}
ah, I see it in angularfire.js. Line 298 wraps adding an item in a $timeout. That's causing the initalCb() to get called before the data has been added to the collection. I pulled the $timeout and it worked. However, then I had to call $scope.$apply() to reflect the added items. I ended up passing scope into angularFireCollection to be sure $apply() gets called. No idea what else I broke by pulling the timeout.
this is a view of the issue: plunkr
EDIT:
as far as I can tell and showed in the plunkr, this doesn't work.
$scope.staff = angularFireCollection(stf, function() {
console.log($scope.staff.length);
});
and while pulling the $timeout from angularfire.js did fix that particular issue, it caused all kinds of other headaches with databinding(as expected), so I put it back. It seems the way to go is to use the snapshot that is passed in the callback. I can't find a lot of documentation on it, but here's what worked for me:
$scope.staff = angularFireCollection(stf, function(snapshot) {
angular.forEach(snapshot, function(item){
//will let you iterate through the data in the snapshot
});
});

Categories

Resources