Chapter 8: Databases, Part 2

Databases, Part 2

In this chapter, we’re going to finish building the remaining features of the original Leaderboard application, and also start working on some new features.

To begin, we’re going to create a “Give 5 Points” button that, when clicked, will increment the value of the selected player’s score field.

To do this, create a button inside the “leaderboard” template:

<button class="increment">Give 5 Points</button>

This button needs to be outside of the each block and have a class attribute of “increment”. I’m placing mine within the ul element, between a pair of li tags, so it’s part of the list itself.

To make this button do something, switch to the JavaScript file and add the following event to the events block:

'click .increment': function(){
    // code goes here
}

The code inside this event will trigger whenever a user clicks the “Give 5 Points” button, and the entire block should now resemble:

Template.leaderboard.events({
    'click .player': function(){
        var playerId = this._id;
        Session.set('selectedPlayer', playerId);
    },
    'click .increment': function(){
        // code goes here
    }
});

(Don’t forget to separate events with commas.)

Within the “click .increment” event itself, grab the unique ID of the selected player by using the Session.get function and output the ID to the Console:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    console.log(selectedPlayer);
}

Based on this code, users can now select a player, click the “Give 5 Points” button, and have the ID of that player appear in the Console.

This lays the foundation for the next step.

Mongo Operators

At this point, we want to make it so, when a user selects a player from the list and clicks the “Give 5 Points” button, the score field of that player is incremented by a value of “5”.

To do this, remove the console.log statement from the “click .increment” event, and replace it with the following:

PlayersList.update();

This update function allow us to modify a Mongo document that’s already saved in a collection, and between the parentheses we can define:

  1. What specific document (player) we want to modify.
  2. How exactly we want to modify that document.

To do this, pass the ID of the selected player into the update function:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    PlayersList.update({ _id: selectedPlayer });
}

Here, the update function is selecting the document we want to modify by referring to the value of the “selectedPlayer” session.

Then, to modify the document, we can pass another argument into the update function that states what part of the document we want to change:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    PlayersList.update({ _id: selectedPlayer }, { score: 5 });
}

Here, we’ve used the JSON format to pass through a reference to the document’s score field, along with a value of “5” to be assigned to that field.

But there’s a problem, because if you test this feature, you’ll notice that it’s broken.

Save the file, switch back to Chrome, select one of the players in the list, and click the “Give 5 Points” button. You’ll notice that the name of that player disappears. The value of the document’s score field will change to “5”, just as planned, but the name field will be completely removed from the document.

This might seem like an error but, by default, the update function works by deleting the original document that’s being updated and then creating an entirely new document with the data that we specify. The value of the _id will remain the same during this process, but because we’ve only specified data for the score field inside the update function, that’s the only other field that will continue to exist after the document is updated.

To account for this, we need to use a Mongo feature that allows us to set the value of the score field without deleting the original document.

To do this, change the update function to the following:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    PlayersList.update({ _id: selectedPlayer }, { $set: });
}

Here, we’ve passed this $set operator through the update function as the second argument, and this operator allows us to modify the value of a field – or multiple fields – without deleting the original document.

After the colon, all we have to do is pass through the fields we want to modify, along with their new values:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    PlayersList.update({ _id: selectedPlayer }, { $set: {score: 5 }});
}

Because of this change, the update function won’t be completely broken. If we select a player and click the “Give 5 Points” button, the value of the score field inside that player’s document will change to “5” without affecting the rest of the document.

But we still haven’t built the feature we wanted to build, because at the moment, we’re only setting the value of the score field, rather than incrementing the value. As such, no matter how many times we click the “Give 5 Points” button, the value of the score field will never change to anything other than “5”.

To fix this problem, replace the $set operator with an $inc operator:

'click .increment': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    PlayersList.update({ _id: selectedPlayer }, { $inc: {score: 5} });
}

Based on this change, whenever the update function is triggered, the value of the selected player’s score field will be incremented by a value of “5”.

What’s also neat is how easily we can allow users to decrement the scores of players by writing some very similar code.

Inside the “leaderboard” template, create a “Take 5 Points” button with a class of “decrement”:

<button class="decrement">Take 5 Points</button>

(I’m placing this button beside the “Give 5 Points” button.)

Then, inside the JavaScript file, create a copy of the “click .increment” event, make sure to separate the events with commas:

Template.leaderboard.events({
    'click .player': function(){
        var playerId = this._id;
        Session.set('selectedPlayer', playerId);
    },
    'click .increment': function(){
        var selectedPlayer = Session.get('selectedPlayer');
        PlayersList.update({ _id: selectedPlayer }, {$inc: {score: 5} });
    },
    'click .increment': function(){
        var selectedPlayer = Session.get('selectedPlayer');
        PlayersList.update({ _id: selectedPlayer }, {$inc: {score: 5} });
    }
});

At this point, we only need to make two changes:

First, change the selector of the newly created event from .increment to .decrement.

Second, pass a value of “-5” into the $inc operator, rather than a value of “5”. This reverses the functionality of the operator, meaning the $inc operator will now decrement the value of the score field.

The final code for this event should now resemble:

'click .decrement': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    PlayersList.update({ _id: selectedPlayer }, {$inc: {score: -5} });
}

The code we’ve written is somewhat bloated – we have two events that are almost identical in functionality, aside from the couple of changes that we just described – but this is something we’ll fix in a later chapter.

Sorting Documents

By default, the players in our list are ranked by the time they were added to the “PlayersList” collection, rather than being ranked by their scores.

To fix this, we need to modify the return statement that’s inside the “player” helper function, which currently looks like this:

'player': function(){
    return PlayersList.find();
}

To make these changes, start by passing a pair of curly braces into the find function:

'player': function(){
    return PlayersList.find({});
}

By using these curly braces, we’re explicitly stating that we want to retrieve all of the data from the “PlayersList” collection. This is the default behavior of the find function, so both of these statements are technically the same:

return PlayersList.find();
return PlayersList.find({});

But by passing through the curly braces as the first argument, we can pass through various options as the second argument, which is what will allow us to define how we want to sort the data that’s retrieved.

As the second argument, pass through a sort method:

return PlayersList.find({}, { sort: });

This method allows us to define how we want to sort the documents that are retrieved by this find function.

Because we want to sort the documents by the values of their score fields, pass through the name of the score field and a value of “-1”:

return PlayersList.find({}, { sort: {score: -1} });

By passing through a value of “-1”, we’re telling the sort method to sort the documents by the value of their score fields in descending order. This means players will be sorted from the highest score to the lowest score. If we passed through a value of “1”, players would be sorted from the lowest score to the highest score.

Based on this change, players will now be ranked based on their score.

But what happens if two players have the same score?

Take “Bob” and “Bill”, for instance. If they have the same score, Bill should be ranked above Bob because, alphabetically, his name comes first. But at the moment, that won’t happen because Bob was added to the collection before Bill.

To fix this, pass a reference to the name field into the sort method, along with a value of “1”:

return PlayersList.find({}, { sort: {score: -1, name: 1} });

The players will still be primarily sorted by their scores, from highest to lowest, but once that sorting has occurred, they’ll be additionally sorted by their names in ascending (alphabetical) order.

Now, if Bob and Bill have the same scores, Bill will be ranked above Bob.

Individual Documents

When a user selects one of the players in the list, that player’s name should appear beneath the list.

To achieve this, create a “selectedPlayer” helper function for the “leaderboard” template:

'selectedPlayer': function(){
    // code goes here
}

Inside the function, retrieve the ID of the currently selected player with the Session.get function, and store this value in a “selectedPlayer” variable:

'selectedPlayer': function(){
    var selectedPlayer = Session.get('selectedPlayer');
}

Then write a return statement that uses a findOne function to retrieve the specific document of the selected player:

'selectedPlayer': function(){
    var selectedPlayer = Session.get('selectedPlayer');
    return PlayersList.findOne({ _id: selectedPlayer });
}

We haven’t talked about the findOne function yet, but the main advantage of this function relates to performance. Because while the find function will search through the collection for all possible matches to a query, the findOne function will stop searching as soon as a single match is found. As such, if you only need to retrieve a single document, it’s best to use the findOne function.

With this function in place, switch to the HTML file and place a reference to the “selectedPlayer” function from inside the “leaderboard” template. I placed mine at the bottom of my player’s list, between a pair of li tags:

<li>Selected Player: {{selectedPlayer}}</li>

But if we save the file, the output won’t look quite right, and that’s because the findOne function is retrieving the player’s entire document.

To fix this, we need to specify that we only want to show the value of the document’s name field, which can be done with dot notation:

<li>Selected Player: {{selectedPlayer.name}}</li>

The interface will then resemble the following:

We should also make it so, if a player isn’t selected, the template doesn’t attempt to display a player’s name or the buttons that are used to interact with the selected player.

This can be done with a simple conditional in the Spacebars syntax:

{{#if selectedPlayer}}
    <li>Selected Player: {{selectedPlayer.name}}</li>
    <li>
        <button class="increment">Give 5 Points</button>
        <button class="decrement">Take 5 Points</button>
    </li>
{{/if}}

As a result, the content within this conditional will only appear when a user selects a player.

Summary

In this chapter, we’ve learned that:

  • By default, the update function deletes the document that’s being updated and recreates it with the specified fields (while retaining the primary key).
  • To change the values of a document without deleting it first, use the $set operator. This operator will only change the values of the specified fields without affecting the rest of the document.
  • The $inc can be used to increment and decrement the value of a numeric field, depending on whether a positive or negative value is passed into it.
  • The sort operator can be used to sort the data that’s returned by the find function, and we can sort by multiple fields at once.
  • If you need to retrieve a single document from a collection, the findOne function is more efficient than the find function.

To gain a better understanding of Meteor:

  • Check out the Operators“ section of the Mongo documentation to see what can be achieved through database operations.

To browse the project’s code in its current state, click here.