Creating a Leaderboard Using Data Retrieved from Firebase

Creating a Leaderboard Using Data Retrieved from Firebase
0

#1

Hi all,

I was on a week or two ago asking about data in Firebase. Thanks to the help on this forum and a lot of google, I was able to get my database set up and I am now retrieving data from the Firebase database.

My new question is, how do I retrieve this data and then use the data to build a leaderboard. Everytime a score is added/updated, I want to loop through this data, find everyone who has a score (the count variable) and then print out the username and score. I have tried a number of solutions and currently the best I can do is adding every user with a score to the div everytime a score is added/updated. But I only want one entry per person and I want the entry to update each time the score changes.

Here is what my code looks like so far:

<div class="leaderboard">
  <p>Leaderboard</p>
  <ul id="leaderboard">
  </ul>
</div>

<script>
let leadsRef = database.ref('users');
let leaderboard = document.querySelector('#leaderboard');
let array = [];

// I get the data using this function
leadsRef.on('value', function (data) {
  var childData = childSnapshot.val();
  
  // childData is now a bunch of typeof objects that looks like this:
  // { count: 0, username: 'John' }
  // now, I try to loop through each of these objects and add them to the leaderboard

  if (childData.count >= 0) {
    let li = document.createElement('li');
    let text = document.createTextNode(childData.username + ' has a score of ' + childData.count);
    li.appendChild(text);
    leaderboard.appendChild(li);
  }

  // this leads to every user and score being printed over and over again whenever the count increases for a user
  // so I tried adding leaderboard.innerHTML = ''; before I create the li, and that doesn't work either
}

</script>

Does anyone have any ideas?

Thanks!


#2

If I am understanding you correctly, childData is an array of objects like the following?

[
  { count: 1, username: 'John' },
  { count: 10, username: 'Tom' },
  { count: 4, username: 'Bob' }
]

In the array, can there be two or more objects with the same username like the following?

[
  { count: 1, username: 'John' },
  { count: 10, username: 'Tom' },  // 1st object with username = 'Tom'
  { count: 4, username: 'Bob' },
  { count: 5, username: 'Tom' } // 2nd object with username = 'Tom'
]

#3

So, I get really confused by this, but I don’t think that childData is an array. I think it just returns an object for each? When I console.log(typeof childData) it returns object and when I console.log(childData) it returns {count: whatever, username: whatever}.

Does that help at all?

There also could be two or more objects with the same username, although in the very limited demonstration I’m doing, there will not be.


#4

I have not used Firebase, but after reading through the documentation, it seems anytime an object of ‘users’ changes, childData will contain the object. So, it seems you need to keep track of whenever you already have one of these user’s li elements displaying on your page. There are a couple of approaches you could take, but I will show you just one.

#1) Create a separate variable (let’s call it userCounts) which will also be an object containing usernames as properties and counts as values for those properties. It would look something like the following. You will need to assign an blank object to a variable at the beginning of your code to hold these usernames.

{
 'Tom': 3,
  'Bob': 2,
  'Julie': 1
}

#2) Then, inside your callback function, you would check if childData.username exists in your object. If it does not exist, that means you have not displayed it yet, so you need to add a new username and corresponding count value to userCounts. Also, you will then need to create an li element with what ever content you want and assign the username to the id attribute. Adding the username to an id will give you a way to modify this element’s contents at a later time if the count changes.

If the username did exist already in userCounts, then update the object by the amount in childData.count and then replace the the existing li element’s text with an updated version which includes the new count. How do you update the existing li? You reference the id of the username you created in step #2 above.

The above procedure should prevent the username from appearing more than once and the current count will always be up to date.

See if you can implement what I have said.

One question about what you wrote above. If the username appears twice in your ‘users’ reference, is it the same user or a different user? If different, then it will be impossible to determine between the different users, because the count on your page for that username will be a combined count. If that is the case, you need to structure your database differently, so that you only have one username record per user.


#5

Thank you so much for your help. I have been able to implement this and it is working better than before. I think I’m not understanding the part about how to update the data. Here is the code that I’ve wrote based on your response. The current result is: when the player first starts the game, the leaderboard shows all of the people who’ve scored in the game before, which is what I want. But, when the new player scores, it just adds all of the players and scores over and over again.

const userCounts = {};

leadsRef.on('value', function (data) {
            
            data.forEach(function (childSnapshot, index) {
                var childData = childSnapshot.val();
                
                if (childData.count > 0) {
                    if (userCounts.hasOwnProperty(childData.username)) {
                        //
                    } else {
                        userCounts[childData.username] = childData.count;
                    }
                }
                          
            }); // this is the end of the foreach
            
            Object.keys(userCounts).forEach(function(key) {
                console.log(key, userCounts[key]);
                let li = document.createElement('li');
                li.id = key.toLowerCase();
                li.innerHTML = key + ' scored ' + userCounts[key];
                leaderboard.appendChild(li);
            });
        });

#6

Where is the code that shows all of the people who have ever scored in the game before? That code should already have populated the userCounts object. Otherwise, you will end up adding the same username at least one if it appears via the on value change callback function.

EDIT: Also, in the callback function, data is not an array (it is an object), so you can not use the forEach on it. You would only need to iterate through an array if more than one object is contained in data. I thought you said that only an object like {username: ‘Tom’, count: 3} would be contained in data.


#7

If you are unable to figure it out, you can click on the blurred code below. See my comments in the code.

/*
I have used ES6 syntax (arrow functions) since you were already using the let keyword.   
*/ 
const changeHTML = (li,user,count) =>  li.innerHTML = user + ' scored ' + count;

const addLI = (user,count) => {
  const li = document.createElement('li');
  li.id = user;
  changeHTML(li, user, count);
  leaderboard.appendChild(li);
};

const replaceLI = (user, count) => {
  let li = document.getElementById(user);
  changeHTML(li, user, count);
};

leadsRef.on('value', data => {
  const childData = data.val();
  let user = childData.username, count = childData.count;
  if (userCounts.hasOwnProperty(user)) { // user already exists
    userCounts[user] += count; // need to update user's count
    replaceLI(user, userCounts[user]);  
  }
  else
    addLI(user, count);
});

/*
I used the code below to simulate the results of code you should already have to create the original userCounts object from the users reference.  I make use of addLI which is a function to display a user's score in an li element.
*/
const userCounts = {Tom: 4, Joe: 2, Terry: 0};
Object.keys(userCounts).forEach(function(user) {
  addLI(user,userCounts[user]);
});

#8

Hi!

Here is what I ended up doing.

 leadsRef.on('value', function (data) {
            
            data.forEach(function (childSnapshot, index) {
                var childData = childSnapshot.val();
                
                if (childData.count > 0) {
                    if (userCounts.hasOwnProperty(childData.username)) {
                        newId = childData.username.toLowerCase();
                        let userCountHolder = document.getElementById(newId);
                        userCountHolder.innerHTML = childData.count;
                        console.log(userCountHolder);
                    } else {
                        userCounts[childData.username] = childData.count;
                        console.log(userCounts);
                        let li = document.createElement('li');
                        newId = childData.username.toLowerCase();
                        li.innerHTML = childData.username + ' scored ' + '<span id="' + newId + '">' + childData.count + '</span>';
                        leaderboard.appendChild(li);
                    }
                }
                
            }); // this is the end of the foreach
            
        });

It works right now but the method that you’ve suggested seems cleaner.

Thank you so much for your help, I really appreciate it!


#9

Glad you got it working. I was thinking data would only be a single object. Since you are using data.forEach, then I assume data can be an array of objects. Is that correct?


#10

I’m honestly not sure. When I try typeof data is returns object?


#11

arrays are objects too. Type console.log(Array.isArray(data))


#12

It returned false when I tried that.


#13
leadsRef.on('value', function (data) {
    console.log(Array.isArray(data));       
    data.forEach(function (childSnapshot, index) {

If the above displays false whenever the count changes for a user, then it should error out, because forEach only works for arrays. Do you see anything in the console?


#14

It returns false in both spots.

leadsRef.on('value', function (data) {
            console.log(Array.isArray(data));
            
            data.forEach(function (childSnapshot, index) {
                var childData = childSnapshot.val();
                console.log('data ', Array.isArray(data));

#15

Just out of curiosity, can you do one more thing for me? Put the following before the forEach line and paste the results of what you see in the console into your next reply.

console.log(JSON.stringify(data));

#16

It returns:

{“Aimeiqn2fr4xKUZSM463”:{“count”:2,“username”:“J”},“HIXvoUgliXS0HFKtHhcuM9sAjOD2”:{“count”:9,“username”:“A”},“J0jmvyamugMBZt9QDjj2”:{“count”:9,“username”:“K”},“LZb9WKKokyyjiNbwUup2”:{“count”:0,“username”:“J”},“OdpkjRt2xdhneJICe5G2”:{“count”:9,“username”:“An”},“a70hJDZ91etlJr6r5lB3”:{“count”:4,“username”:“R”}}

{"Aimeiqn2fr4xKUZSM463":{"count":2,"username":"J"},"HIXvoUgliXS0HFKtHhcuM9sAjOD2":{"count":9,"username":"A"},"J0jmvyamugMBZt9QDjj2":{"count":9,"username":"K"},"LZb9WKKokyyjiNbwUup2":{"count":0,"username":"J"},"OdpkjRt2xdhneJICe5G2":{"count":9,"username":"An"},"a70hJDZ91etlJr6r5lB3":{"count":4,"username":"R"}}

EDIT: I don’t know if this is useful at all, but for whatever reason, the index parameter in the forEach always returns undefined, when I would normally expect it to return the index number.


#17

OK, I learned something. When I read the Firebase documentation (which I should have thought about before), there is a forEach method which can be used on a DataSnapshot (what data is in your code).

https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#forEach

It has nothing to do with Array.prototype.forEach, which explains why it did not error out and that you can not access the index.

Know this new information, the following could be used in my original solution.

leadsRef.on('value', data => {
  data.forEach(function (childSnapshot) {
    const childData = childSnapshot.val();
    let user = childData.username, count = childData.count;
    if (userCounts.hasOwnProperty(user)) { // user already exists
      userCounts[user] += count; // need to update user's count
      replaceLI(user, userCounts[user]);  
    }
    else
      addLI(user, count);
  });
});