I have a small app where a user can input a persons name and add them to a board and then users can up or down vote them (for fantasy football). Currently, the only thing that happens immediately without need for refreshing the page is when a player is added via the text input box. This does a little fade-in for the object and it comes right to the screen:
addPlayer(player) {
this.state.user ?
this.database.push().set({ playerContent: player, votes: 0})
:
console.log("Not Logged In")
}
Is this done in real time because it is a push? How could I have the board of players update after a user casts a vote? Right now, when say Jane Doe has two votes, and John Doe has 2 votes and you vote John Doe up to 3, he doesn't jump up ahead of Jane Doe until after you refresh the page. Is there a way to refresh each individual object as you cast a vote? Here's my voting code
upvotePlayer(playerId) {
if(this.state.user) {
let ref = firebase.database().ref('/players/' + playerId + '/voters');
ref.once('value', snap => {
var value = snap.val()
if (value !== null) {
ref.child(this.uid).once('value', snap => {
if (snap.val() === 0 || snap.val() === -1 || snap.val() == null){
ref.child(this.uid).set(1);
} else if (snap.val() === 1) {
ref.child(this.uid).set(0);
}
else {
console.log("Error in upvoting. snap.val(): " + snap.val())
}
})
} else {
console.log("Doesn't exist")
ref.child(this.uid).set(1);
}
});
}
else {
console.log("Must be logged in to vote.")
}
}
If I'm missing relevant code please let me know and I'll supply it. I tried calling componentDidMount after the vote is cast and this.forceUpdate() (even if it is discouraged) just to see if it was a solution, which it didn't seem to be.
Heres the player component which is rendered in App.js
{
orderedPlayersUp.map((player) => {
return (
<Player
playerContent={player.playerContent}
playerId={player.id}
key={player.id}
upvotePlayer={this.upvotePlayer}
downvotePlayer={this.downvotePlayer}
userLogIn={this.userLogIn}
userLogOut={this.userLogOut}
uid={this.uid}
/>
)
})
}
Edit
When I change the upvotePlayer function's database call to .on rather than .once and proceed to upvote a player, it spews out an infinite amount of my console.logs from this componentWillMount function:
App.js line 51 is the console.log below.
this.database.on('child_changed', function (snapshot) {
var name = snapshot.val();
console.log("Player: " + name.playerContent + " has " + name.votes + " votes.")
})
After 20 seconds or so when it overloads itself, an error page comes up and says RangeError: Maximum call stack size exceeded
Changing the componentWillMount functions database call to this.database.once instead of .on cuts down on the infinite loop, but still throws RangeError: Maximum call stack size exceeded
The error:
A gist with the App.js (where the voting occurs (upvotePlayer)) and Player.jsx. Gist with App.js and Player.jsx
Edit2
Votes started registering immediately to the screen when I changed from .once to .on in the Player.jsx file in the comondentDidMount() as seen below. To clarify, this solves my issue of the voting icons not changing in real-time when I change from upvote to downvote. I have yet to test whether or not it registers one player overtaking another with more votes as I originally asked. Just wanted to clarify for anyone who may look through this code to help themselves.
componentDidMount() {
let ref = firebase.database().ref('/players/' + this.playerId + '/voters');
ref.on('value', snap => {
var value = snap.val()
if (value !== null && this.props.uid !== null) {
ref.child(this.props.uid).on('value', snap => {
if (snap.val() === 1){
this.setState({ votedUp: true, votedDown: false })
}
else if (snap.val() === 0) {
this.setState({ votedUp: false, votedDown: false })
}
else if (snap.val() === -1) {
this.setState({ votedDown: true, votedUp: false })
}
else {
console.log("Error calculating the vote.")
}
})
} else {
//Error
}
});
}
Your firebase call is a .once() call at the moment. That will only call the database once. If you want your component to continue to listen for changes then you need to switch to a .on() call. So that would be ref.child(this.uid).on(...) instead of ref.child(this.uid).once(...)
ref.child(this.uid).on('value', snap => {
if (snap.val() === 0 || snap.val() === -1 || snap.val() == null){
ref.child(this.uid).set(1);
} else if (snap.val() === 1) {
ref.child(this.uid).set(0);
}
else {
console.log("Error in upvoting. snap.val(): " + snap.val())
}
})
Related
So I need a command to send 4 different messages to the user, each message with a new prompt, so for example "Prompt 1" and what ever the user responds will be pushed into an array called "config". I thought about using message collectors, but couldn't set it up to collect multiple answers.
Pseudo code:
let config = new Array();
message.author.send("Prompt 1");
config.push(collected.answer).then(
message.author.send("Prompt 2");
config.push(collected.answer).then(
ect...
)
You CAN use message collectors. However, you need to have it in a variable. Here is an example:
msg.author.send('some prompt').then(m => {
let i = 0;
var collector = m.channel.createMessageCollector(me =>
me.author.id === msg.author.id && me.channel === m.channel, {max: /*some number*/})
collector.on('collect', collected => {
if(collected.content === 'end') return collector.stop();
//basically if you want to stop all the prompts and do nothing
i +=1
if(i === 1) return collected.channel.send(/*something*/); //next prompt
if(i === 2) return collected.channel.send(/*something*/); //and so on until you get to the last prompt
})
collector.on('end', collectedMsgs => {
if(collectedMsgs.size < /*amount of prompts*/) {
return collectedMsgs.first().channel.send('Ended early, nothing was done.');
}
//some action you would do after all are finished
})
})
There may be some missing parentheses, you will have to add them.
Purpose of my code, is to fire fetchNumbers() (that fetches numbers from API) when a user scrolls bottom of the page, some kind of infinite scroll. I'm having issue with condition inside axios promise (then), because it fires two console.log outputs at the same time. Seems like condition is ignored at all.
Method i'm using:
methods: {
fetchNumbers (type = 'default', offset = 0, limit = 0) {
return axios.get(globalConfig.NUMBERS_URL)
.then((resp) => {
if (type === 'infinite') {
console.log('infinite fired')
} else {
console.log('default fired')
}
})
}
Mounted function (where i suspect the issue):
mounted () {
window.onscroll = () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight > document.documentElement.offsetHeight - 1
if (bottomOfWindow) {
this.fetchNumbers('infinite')
}
}
}
When i reload the page, or enter it, i'm getting 2 console outputs at the same time:
default fired
infinite fired
Sometimes the order is reversed.
UPDATE.
Methods that calling fetchNumbers()
async created () {
await this.fetchNumbers()
}
showCats (bool) {
this.showCategories = bool
if (!bool) {
this.category = [1]
} else {
this.category = []
}
this.isActive = true
this.fetchNumbers()
}
UPDATE 2.
Found the culprit - would post it in an answer.
UPDATE 3.
Issue is indeed with onscroll function. I have 3 pages in my APP: main page, numbers page, contact page. If i go to the numbers page (where onscroll function is mounted), then go to main or contact page, then this onscroll function is still attached and when i reach the bottom - it fires my api call, even if it's not the numbers page. Is it possible to limit this function only to numbers page?
I had to disable onscroll listener on destroy:
destroyed () {
window.onscroll = null
}
So when i visit the main or contact page, that listener won't be attached.
Also i had to move onscroll listener from mounted to created, because when it was in mounted () it was firing twice:
created () {
window.onscroll = () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight > document.documentElement.offsetHeight - 1
if (bottomOfWindow) {
this.isActive = true
this.fetchNumbers('infinite', counter++ * this.limit)
}
}
}
fetchNumbers is most definetly called twice.
Try log every context as type.
fetchNumbers (type = 'default', offset = 0, limit = 0) {
return axios.get(globalConfig.NUMBERS_URL)
.then((resp) => {
if (type === 'infinite') {
console.log(type,'infinite fired'); //this will always print 'infinite', 'inifinite fired'
} else {
console.log(type,'default fired'); //this will print the caller's name and 'default fired'
type = 'default'; //after logging reset type to 'default' to continue the execution
}
})
}
async created () {
await this.fetchNumbers('created');
}
showCats (bool) {
this.showCategories = bool
if (!bool) {
this.category = [1];
} else {
this.category = [];
}
this.isActive = true
this.fetchNumbers('showCats');
}
Either created or showCats is being fired for some reason at the same time as window.onscroll. The randomly changing order suggests a race condition between the two methods.
UPDATE
This should work, provided you nested the desired page in a div with id="number_page_div_id" (you may probably have to adjust that elements height and position):
created () {
let element = getElementById('numbers_page_div_id');
element.onscroll = () => {
let bottomOfWindow = element.scrollTop + window.innerHeight > element.offsetHeight - 1;
if (bottomOfWindow) {
this.fetchNumbers('infinite')
}
}
}
you may approach separate condition instead of if/else e.g.
.then((resp) => {
if (type === 'infinite') {
console.log('infinite fired')
}
if (type === 'default '){
console.log('default fired')
}
})
I'm using Javascript (Ionic Framework) to load groups of Firebase sets (like Instagram), but I need to also filter the Firebase sets based on the value of the "sold" key in each data set. Here is one data set (there are many such sets):
The idea is if "sold" is false, then we load a given data set, if it's true, then we should not load it and skip it.
But there is a catch which makes this problem difficult. Please see the code next:
doInfinite(infiniteScroll) {
if (this.end === false && (this.counter === 3 || this.counter === 8) && this.startListingID != undefined) {
setTimeout(() => {
firebase.database().ref('/explore/').orderByKey().startAt((this.startListingID).toString()).limitToFirst(6).once('value', (snapshot) => {
snapshot.forEach(snap => {
if((this.counter <= (this.counter2 + 4)) && (this.intermediateListingID !== snap.val().listingID) && this.end === false) {
let temp = {
Item: snap.val().Item,
Price: snap.val().price,
displayName: snap.val().displayName,
sold: snap.val().sold,
listingID: snap.val().listingID }
this.object.push(temp);
this.counter++;
this.intermediateListingID = snap.val().listingID;
return false;
} else {
if(this.startListingID !== snap.val().listingID) {
this.counter = this.counter2;
this.startListingID = snap.val().listingID;
}
else { this.end = true; }
}
setTimeout(() => {
infiniteScroll.complete();
}, 1000);
});
setTimeout(() => {
infiniteScroll.complete();
}, 1000);
}).catch((error) => { console.log("infiniteScroll Error: " + JSON.stringify(error)); infiniteScroll.complete(); });
}, 1000);
} else {
setTimeout(() => {
infiniteScroll.complete();
}, 1000);
}
}
I am just starting with coding, so I do realize the code above is rather bad, but it works.
The problem arises in cases where I would get "sold" data sets, and then the infinite scroll which is limitToFirst(6) turns to be less and the whole numbering logic screws up, not to mention if I get 6 sold data sets in a row, then nothing is shown and the scroll just breaks down.
If there a way (ideally on the level of Firebase) to FILTER all data sets with "sold = true" before they are fed into my infinite scroll logic?
Many thanks!
You could replace orderByKey with orderByChild('sold').equalTo(true)
firebase.database().ref('/explore/').orderByChild('sold').equalTo(true)
.limitToFirst(6).once('value', (snapshot) => {...
This should give you 6 items where sold = true. Unfortunately, you can't combine two filtering queries. So you'll either have to pull all the items and then just sort out which ones are sold after, or grab all the sold ones in random order. Though, if you're sorting by key this leads me to believe you don't need them sorted in any particular order, so you could go with this.
I am fairly new to meteor, and I am running into a strange issue with subscribe callbacks. I have a database containing courses and reviews. I'm using a publish/subscribe model on the reviews to return reviews that are only relevant to a selected class, and I want this to change each time a new class is clicked on. I want to print all the reviews and compile some metrics about the reviews (average quality, difficulty rating). Using the following code, with a subscribe that updates the reviews sent to the client, the printed reviews (which are grabbed from a helper) return correctly, but the metrics (which are grabbed on an onReady callback to the helper) are inaccurate. When the onReady function is run, the current result of the local reviews collection contains the union of the clicked class and the previously clicked class, even though the reviews themselves print correctly.
I've also tried using autoTracker, but I got the same results. Is there a way to clear previous subscribe results before updating them?
publish:
Meteor.publish('reviews', function validReviews(courseId, visiblity) {
console.log(courseId);
console.log(visiblity);
var ret = null
//show valid reviews for this course
if (courseId != undefined && courseId != "" && visiblity == 1) {
console.log("checked reviews for a class");
ret = Reviews.find({class : courseId, visible : 1}, {limit: 700});
} else if (courseId != undefined && courseId != "" && visiblity == 0) { //invalidated reviews for a class
console.log("unchecked reviews for a class");
ret = Reviews.find({class : courseId, visible : 0},
{limit: 700});
} else if (visiblity == 0) { //all invalidated reviews
console.log("all unchecked reviews");
ret = Reviews.find({visible : 0}, {limit: 700});
} else { //no reviews
console.log("no reviews");
//will always be empty because visible is 0 or 1. allows meteor to still send the ready
//flag when a new publication is sent
ret = Reviews.find({visible : 10});
}
//console.log(ret.fetch())
return ret
});
subscribe:
this.helpers({
reviews() {
return Reviews.find({});
}
});
and subscribe call, in constructor with the helpers:
constructor($scope) {
$scope.viewModel(this);
//when a new class is selected, update the reviews that are returned by the database and update the gauges
this.subscribe('reviews', () => [(this.getReactively('selectedClass'))._id, 1], {
//callback function, should only run once the reveiws collection updates, BUT ISNT
//seems to be combining the previously clicked class's reviews into the collection
onReady: function() {
console.log("class is: ", this.selectedClass);
if (this.isClassSelected == true) { //will later need to check that the side window is open
//create initial variables
var countGrade = 0;
var countDiff = 0;
var countQual = 0;
var count = 0;
//table to translate grades from numerical value
var gradeTranslation = ["C-", "C", "C+", "B-", "B", "B-", "A-", "A", "A+"];
//get all current reviews, which will now have only this class's reviews because of the subscribe.
var allReviews = Reviews.find({});
console.log(allReviews.fetch());
console.log("len is " + allReviews.fetch().length)
if (allReviews.fetch().length != 0) {
console.log("exist")
allReviews.forEach(function(review) {
count++;
countGrade = countGrade + Number(review["grade"]);
countDiff = countDiff + review["difficulty"];
countQual = countQual + review["quality"];
});
this.qual = (countQual/count).toFixed(1);
this.diff = (countDiff/count).toFixed(1);
this.grade = gradeTranslation[Math.floor(countGrade/count) - 1];
} else {
console.log("first else");
this.qual = 0;
this.diff = 0;
this.grade = "-";
}
} else {
console.log("second else");
this.qual = 0;
this.diff = 0;
this.grade = "-";
}
}
})
When using pub-sub the minimongo database on the client will contain the union of subscriptions unless they are explicitly cleared. For that reason you want to repeat the query that's in the publication on the client side so that you filter and sort the same way. Minimongo is very fast on the client and you typically have much less data there so don't worry about performance.
In your constructor you have:
var allReviews = Reviews.find({});
instead use:
var allReviews = Reviews.find(
{
class : (this.getReactively('selectedClass'))._id,
visible : 1
},
{limit: 700}
);
Another side tip: javascript is quite clever about truthy and falsy values.
if (courseId != undefined && courseId != "" && visibility == 1)
can be simplified to:
if (courseId && visibility)
assuming you're using visibility == 1 to denote true and visibility == 0 to denote false
I am trying to display "No Results Found" in the case where someone uses my search feature to search for something that returns no results from the database. The problem I'm running into is that "No Results Found" prints to the screen right away - then disappear when a search query is actually being evaluated - and then reappears if no results were found. In other words, it's working as it should EXCEPT that it should wait till a query is actually triggered and evaluated before printing "No Results Found" to the screen. Conceptually what's the best way to do this? On the one hand it occurs to me that I could just use a timeout. But that's not really solving the problem directly. So what would be the best way to approach this? This is the code I have for the function:
public get noResultsFound(): boolean
{
if (this.query && !this._isSearching && !this.hasResults) {
return true;
}
}
Here's my view code:
<div *ngIf="inputHasFocus && noResultsFound" class="no-results-found">No Results Found</div>
Promises sound like what you need :D
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
Something along the lines of this.
search = (paramObj) : Promise<SomeDataClass> => {
// Some search logic
}
requestSearch() {
this.search(null).then((result) => {
if (result.length === 0)
{
//logic for showing 0 results here
}
else{
// Show result?
}
})
}
A more complete example would look a bit like this.
class example {
public someBool = false;
public search = (paramObj): Promise<Array<string>> => {
this.someBool = !this.someBool;
return new Promise<Array<string>>((resolver) => {
// This just simulates something taking time.
setTimeout(() => {
if (this.someBool) {
resolver([]);
} else {
resolver(["hi","there"]);
}
}, 1000)
});
}
public requestSearch() {
this.search(null).then((result) => {
if (result.length === 0) {
console.log("empty")
}
else {
console.log(result)
}
})
}
}
So, in the end I was able to resolve this not by using some kind of debounce timeout, but by changing what the function was paying attention to - so to speak. By handling it this way the "No Results Found" never triggers before it's supposed to. This is what I ended up using:
public get noResultsFound(): boolean
{
if (!Object.isNullOrUndefined(this._results) && this._results.length < 1) {
return true;
}
}