— Node.js, JavaScript, Socket.io, Handlebars, intermediate, tutorial, intro-to-node-js — 11 min read
Welcome to the final part of the six-part Introduction to Node.js tutorial series. By the end of this tutorial, you will be able to:
http
Node.js module and with node-fetchSocket.IO Overview
Client-Server Communication Protocols
Building a Live Multi-Player Programming Trivia Game
emit
on
socket.broadcast.to
)io.in.emit
)Additional Resources
Review
Key Takeaways
What's next
From the Socket.IO website:
"Socket.IO enables real-time, bidirectional and event-based communication. It works on every platform, browser or device, focusing equally on reliability and speed."
Socket.IO can be used to build a variety of applications that have real-time communication capabilities, such as streaming applications, document collaboration, and instant messaging.
In this tutorial, we will be building a Socket.IO-powered multi-player trivia game.
AJAX polling, long polling, and WebSockets are popular communication protocols between a client (e.g., browser) and a web server.
Let's review each protocol type briefly, as it's important for understanding how Socket.IO works.
After sending each request, the client waits for the server to respond, either with the data or with an empty response (if no data is available).
This technique significantly increases the HTTP overhead (e.g., creating HTTP headers, establishing and closing connections, generating a response, etc.) because the client has to keep bugging the server for new pieces of data, most of which are just empty responses.
Instead of sending an empty response when information is not available, the server "hangs" or waits until data becomes available to push a response to the client or a timeout threshold is reached.
The client then immediately sends another HTTP request to the server, thereby ensuring that the server would always have a request waiting for it to respond to with any new data. This process is repeated as many times as needed.
The client may have to reconnect to the server periodically after the connection is closed due to timeouts.
"WebSocket is a computer communications protocol, providing full-duplex communication channels over a single Transmission Control Protocol connection" - Wikipedia
WebSocket handshake
.To establish a WebSocket connection, the browser contacts the server and asks for a WebSocket a connection (a process known as the WebSocket handshake
).
If the handshake
succeeds, then the server and client can pass data back and forth at any time, as long as the connection remains open. Responses are event-driven, so there's no need to continually poll the server.
Socket.IO uses WebSocket as a transport when the WebSocket API is available in the browser, falling back on HTTP long polling if it's not. As of July 2020, 97% of all browsers support WebSocket.
The Socket.IO client is a wrapper around the WebSocket API, whereas the Socket.IO server exposes a socket
object that extends the Node.js EventEmitter class.
Take a look at this article if you're interested in a deep dive into WebSockets!
Let's build a live multi-player trivia game using Socket.IO! We'll learn about Socket.IO's various API methods along the way.
The following features will be part of the game:
name
and a room
to join a gameHere's the deployed game for your reference.
If you ever feel lost as you progress through the tutorial, you can always reference the commit link provided at the end of each task to compare your code against the completed version!
You can find the code for the completed project here.
There's no starter code for this tutorial. You will follow the steps described below to build the project from scratch.
A Github repo with the complete code is available here.
Here's a commit showing the changes for task 1.
From your terminal, change into your course directory: $ cd /path/to/your/course/directory
Create a new directory: $ mkdir trivia-game
.
Change into your /trivia-game
directory: $ cd trivia-game
.
Turn your directory into an npm
package and use the information extracted from the current directory to configure your package.json
:
1$ npm init -yes
Running this command creates a package.json
file in your current directory.
Install the required dependencies:
1$ npm install express socket.io
Running this command creates a new folder called node_modules
in your current directory.
Add a .gitignore
file to your current directory:
1$ touch .gitignore
Add node_modules
to the first line of the .gitignore
file to avoid committing node_modules
folder to Git.
Here's a commit showing the changes for task 2.
Create a folder for storing our server-side code: $ mkdir src
.
Create a file for configuring and starting our server: $ touch src/index.js
.
Set up a basic Express server:
1const express = require("express");2const app = express();3
4const port = process.env.PORT || 8080;5app.listen(port, () => {6 console.log(`Server is up on port ${port}.`);7});
Start up your application server: $ nodemon src/index.js
. Your console should tell you that your server is up and running!
Create a public/
folder in project folder: $ mkdir public
.
Set up our server to serve static assets (e.g., HTML, CSS, and scripts for our game pages) to the client.:
1const express = require('express');2+ const path = require('path');34const port = process.env.PORT || 8080;5
6const app = express();7
8+ const publicDirectoryPath = path.join(__dirname, '../public');9+ app.use(express.static(publicDirectoryPath));10+11app.listen(port, () => {12 console.log(`Server is up on port ${port}.`);13});
Here's a commit showing the changes for task 3.
Connect Socket.IO to our Express server:
1const express = require('express');2+ const http = require('http');3const path = require('path');4+ const socketio = require('socket.io');56const port = process.env.PORT || 8080;7
8const app = express();9+ const server = http.createServer(app); // create the HTTP server using the Express app created on the previous line10+ const io = socketio(server); // connect Socket.IO to the HTTP server1112const publicDirectoryPath = path.join(__dirname, '../public');13app.use(express.static(publicDirectoryPath));14
15+ io.on('connection', () => { // listen for new connections to Socket.IO16+ console.log('A new player just connected');17+ })1819- app.listen(port, () => {20+ server.listen(port, () => {21 console.log(`Server is up on port ${port}.`);22});
Here's a commit showing the changes for task 4.
Let's create the files that the client will need to render the game in the browser.
Create folders for our JavaScript files and CSS in /public
: $ mkdir public/js public/css
Create the client-side files: $ touch public/index.html public/trivia.html public/js/trivia.js public/css/styles.css
Your public/
folder should look like this:
Since HTML & CSS are not the primary focus of the course, we will not create these from scratch! Just copy and paste the HTML (i.e., public/index.html
, public/trivia.html
) and CSS code (i.e., public/css/styles.js
) from this commit into your own files. That said, feel free to tweak the styling to your liking!
public/index.html
and public/trivia.html
contain the HTML code for the registration and game pages respectively.
public/trivia.html
includes a few essential third-party scripts for our game:
1<!-- Handlebars: our client-side templating engine -->2<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.6/handlebars.min.js"></script>3
4<!-- MomentJS: a library for formatting timestamps -->5<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>6
7<!-- Client-Side Socket IO -->8<script src="/socket.io/socket.io.js"></script>9
10<!-- Custom JavaScript file where we write our game-related logic -->11<script src="/js/trivia.js"></script>
Socket.IO automatically serves up /socket.io/socket.io.js
which contains the client-side Socket.IO code.
http://localhost:8080
should now display a registration page that includes a form which allows a player to input their name
and room
.
Fill out the form and hit Join
. You should be taken to the game page.
The app does not work yet because we haven't added any of the game logic!
Here's a commit showing the changes for task 5.
The welcome header is currently missing, so let's add that.
Notice in the completed game that the header greets the player by the name that they provided on the registration page. We can use Handlebars
to generate the header dynamically in the client!
Create the welcome header:
1// Extract the playerName from the url search params, which the player provided on the registration page2const urlSearchParams = new URLSearchParams(window.location.search);3const playerName = urlSearchParams.get("playerName");4
5// We'll follow the basic tasks below whenever we need to update the page with Handlebars.6// Target the template which is embedded as a script tag in the `public/trivia.html` file7const mainHeadingTemplate = document.querySelector(8 "#main-heading-template"9).innerHTML;10
11// Compile the template into HTML by calling Handlebars.compile(), which returns a function12const welcomeHeadingHTML = Handlebars.compile(mainHeadingTemplate);13// Insert the welcomeHeadingHTML right after the opening <main> tag14document.querySelector("main").insertAdjacentHTML(15 "afterBegin",16 // Invoke the welcomeHeadingHTML function, passing in the data that will be used to render the heading17 welcomeHeadingHTML({18 playerName,19 })20);
Here's a commit showing the changes for task 6.
Socket.IO uses events
facilitate data transfer between the client and the server.
Every event consists of two sides: the sender and the receiver.
If the server is the sender, then the client is the receiver. If the client is the sender, then the server is the receiver.
socket.emit
Events can be sent from the sender using the socket.emit
function. Let's send an event from the client to notify the server that a player has joined the game:
1+ // io is provided by the client-side Socket.IO library that was loaded in public/trivia.html2+ const socket = io();34const urlSearchParams = new URLSearchParams(window.location.search);5const playerName = urlSearchParams.get('playerName');6+ // Extract the `room` value from the url, as we will need to use it below7+ const room = urlSearchParams.get('room');8.9.10.11+ // Call `socket.emit` to send an event to the server.12+ // The first argument describes the name of the event (i.e., `join`).13+ // The second argument contains the data (i.e., `playerName` and `room`) to be sent to the server along with the event.14+ // The third argument is a callback function that is invoked when the server acknowledges the event.15+ // If there's an error, the player will see an alert and be redirected back to the registration page16+17+ socket.emit('join', { playerName, room }, error => {18+ if (error) {19+ alert(error);20+ location.href = '/';21+ }22+ });
Navigate to http://localhost:8080
on a few separate browser tabs. In your Node.js console, you should see a console message A new player just connected
for each browser tab opened.
To keep our code modular and maintain separation of concerns, let's store all the code related to adding, retrieving, and removing players from a game in a separate file in the server.
Go ahead and create a new folder called /utils
and a file called /players.js
inside that folder:
1const players = [];2
3// Add a new player to the game4const addPlayer = ({ id, playerName, room }) => {5 if (!playerName || !room) {6 return {7 error: new Error("Please enter a player name and room!"),8 };9 }10
11 // clean the player registration data12 playerName = playerName.trim().toLowerCase();13 room = room.trim().toLowerCase();14
15 const existingPlayer = players.find((player) => {16 return player.room === room && player.playerName === playerName;17 });18
19 if (existingPlayer) {20 return {21 error: new Error("Player name is in use!"),22 };23 }24
25 const newPlayer = { id, playerName, room };26 players.push(newPlayer);27
28 return { newPlayer };29};30
31// Get a player by id32const getPlayer = (id) => {33 const player = players.find((player) => player.id === id);34
35 if (!player) {36 return {37 error: new Error("Player not found!"),38 };39 }40
41 return { player };42};43
44// Get all players in the room45const getAllPlayers = (room) => {46 return players.filter((player) => player.room === room);47};48
49// Remove a player by id50const removePlayer = (id) => {51 return players.find((player, index) => {52 if (player.id === id) {53 return players.splice(index, 1)[0];54 }55 return false;56 });57};58
59// Export our helper methods60module.exports = {61 addPlayer,62 getPlayer,63 getAllPlayers,64 removePlayer,65};
DISCUSS: In your breakout group, discuss what the helper functions in src/utils/players.js
do.
Let's also create a function to format game messages so that they include timestamps which we will display along with the trivia answers and chat messages in the game.
1module.exports = (playerName, text) => {2 return {3 playerName,4 text,5 createdAt: new Date().getTime(),6 };7};
Don't forget to import these helper functions in the main server file so we can use them later on:
1const formatMessage = require("./utils/formatMessage.js");2
3const {4 addPlayer,5 getAllPlayers,6 getPlayer,7 removePlayer,8} = require("./utils/players.js");
socket.on
Events can be received by the receiver using socket.on
.
In the server, listen for the join
event within the connection event handler:
1- io.on('connection', () => {2+ io.on('connection', socket => {3 console.log('A new player just connected');45+ // Call `socket.on` to listen to an event.6+ // The first argument describes the name of the event to listen to (i.e., `join`).7+ // The second argument is a callback that runs on receiving the event. This callback has access to two parameters.8+ // The first parameter represents a data object sent from the client along with the event.9+ // The second parameter is a callback function that can be called on the server to trigger the acknowledgement function on the client, a process called Event Acknowledgments10+11+ socket.on('join', ({ playerName, room }, callback) => {12+ const { error, newPlayer } = addPlayer({ id: socket.id, playerName, room });13+14+ // In this example, the callback is called with an error message15+ // if faulty regisration information was detected.16+ // The argument would get passed to the client where the error could be shown.17+ if (error) return callback(error.message);18+ callback(); // The callback can be called without data.19+20+ // The server is responsible for adding and removing players from a room.21+ // Call `socket.join` to subscribe the socket to a given room.22+ socket.join(newPlayer.room);23+24+ // Call `socket.emit` to send the new player a welcome message when the player joins a room25+ // We'll write code in the client later on to listen for the `message` event26+ // Call `formatMessage` to add format our message with timestamps27+ socket.emit('message', formatMessage('Admin', 'Welcome!'));28+ });29});
The client should listen for the message
event and update the chat section accordingly whenever the message
event is received:
1socket.on("message", ({ playerName, text, createdAt }) => {2 // target the container in the DOM where we'll attach the new message to3 const chatMessages = document.querySelector(".chat__messages");4
5 // target the Handlebars template we need to create a message6 const messageTemplate = document.querySelector("#message-template").innerHTML;7
8 // Compile the template into HTML by calling Handlebars.compile(),9 // which returns a function10 const template = Handlebars.compile(messageTemplate);11
12 const html = template({13 playerName,14 text,15 createdAt: moment(createdAt).format("h:mm a"),16 });17
18 // Insert the new html right at the beginning of chatMessages container19 chatMessages.insertAdjacentHTML("afterBegin", html);20});
Test your game page and make sure the Welcome
message is displaying in the chat box at this point!
socket.broadcast.to
)Here's a commit showing the changes for task 7.
Next, use socket.broadcast.to
to send a message
event to let all other players in the room know when a new player has joined the game.
1io.on('connection', () => {2 console.log('A new player just connected');34 socket.on('join', ({ playerName, room }, callback) => {5 const { error, newPlayer } = addPlayer({ id: socket.id, playerName, room });67 if (error) return callback(error.message);89 socket.join(newPlayer.room);1011 socket.emit('message', formatMessage('Admin', 'Welcome!'));1213+ socket.broadcast14+ .to(newPlayer.room)15+ .emit(16+ 'message',17+ formatMessage('Admin', `${newPlayer.playerName} has joined the game!`)18+ );19+20+ });2122});
Navigate to http://localhost:8080
and join the same room from several browser tabs. You should see the chat box update with a new message about the player who has just joined every time a new socket connects to the server.
io.in.emit
)Here's a commit showing the changes for task 8.
The Game Info section displays the player's current room and the players who are active in that room. We can use the io.in.emit
function to send an event (i.e., room
) to all players in the room:
socket.broadcast.to
sends an event to all clients in the room except the sender whereas io.in.emit
sends an event to all clients in the room
NOTE: io.in.emit
and io.to.emit
methods do the same thing!
1io.on('connection', () => {2 console.log('A new player just connected');34socket.on('join', ({ playerName, room }, callback) => {5 const { error, newPlayer } = addPlayer({ id: socket.id, playerName, room });67 if (error) return callback(error.message);89 socket.join(newPlayer.room);1011 socket.emit('message', formatMessage('Admin', 'Welcome!'));1213 socket.broadcast14 .to(newPlayer.room)15 .emit(16 'message',17 formatMessage('Admin', `${newPlayer.playerName} has joined the game!`)18 );1920+ // Emit a "room" event to all players to update their Game Info sections21+ io.in(newPlayer.room).emit('room', {22+ room: newPlayer.room,23+ players: getAllPlayers(newPlayer.room),24+ });25 });26});
We have to set up the client to listen for the room
event and update the page (i.e., the Game Info section) accordingly on receiving the event:
1socket.on("room", ({ room, players }) => {2 // target the container where we'll attach the info to3 const gameInfo = document.querySelector(".game-info");4
5 // target the Handlebars template we'll use to format the game info6 const sidebarTemplate = document.querySelector(7 "#game-info-template"8 ).innerHTML;9
10 // Compile the template into HTML by calling Handlebars.compile(), which returns a function11 const template = Handlebars.compile(sidebarTemplate);12
13 const html = template({14 room,15 players,16 });17
18 // set gameInfo container's html content to the new html19 gameInfo.innerHTML = html;20});
Here's a commit showing the changes for task 9.
Sometimes, players get frustrated and quit the game without saying a word!
Your challenge here is to write code to remove the player from the game, send a message to all other players in the room to let them know of the player's departure, and also update the players list in the Game Info section.
Use the Socket.IO docs to help you.
HINT: What event gets fired when a player disconnects from the game?
Listen for the disconnect
event on the server. This event is automatically fired whenever a socket is disconnected.
1socket.on("disconnect", () => {2 console.log("A player disconnected.");3
4 const disconnectedPlayer = removePlayer(socket.id);5
6 if (disconnectedPlayer) {7 const { playerName, room } = disconnectedPlayer;8 io.in(room).emit(9 "message",10 formatMessage("Admin", `${playerName} has left!`)11 );12
13 io.in(room).emit("room", {14 room,15 players: getAllPlayers(room),16 });17 }18});
Here's a commit showing the changes for task 10.
Let's allow players to communicate with each other in the chat box! In the client, add an event listener on the chat form:
1// First, target the chat form in the DOM2const chatForm = document.querySelector(".chat__form");3
4// Second, add an event listener on the chat form5// The first argument is the event to listen for (i.e., 'submit')6// The second argument is the callback function that is called when the event is triggered7chatForm.addEventListener("submit", (event) => {8 event.preventDefault();9
10 const chatFormInput = chatForm.querySelector(".chat__message");11 const chatFormButton = chatForm.querySelector(".chat__submit-btn");12
13 // Disable the chat form submit button to prevent the player from being able to submit multiple messages simultaneously14 chatFormButton.setAttribute("disabled", "disabled");15
16 // Extract the message that the player typed into the message box17 const message = event.target.elements.message.value;18
19 // Send an event to the server along with the player's chat message20 socket.emit("sendMessage", message, (error) => {21 // On event acknowledgement by the server, reset the chat form button22 chatFormButton.removeAttribute("disabled");23 chatFormInput.value = "";24 chatFormInput.focus();25
26 // Alert an error if an error is received27 if (error) return alert(error);28 });29});
In the server, listen for the sendMessage
event and emit the message to all players:
1socket.on("sendMessage", (message, callback) => {2 const { error, player } = getPlayer(socket.id);3
4 if (error) return callback(error.message);5
6 if (player) {7 io.to(player.room).emit(8 "message",9 formatMessage(player.playerName, message)10 );11 callback(); // invoke the callback to trigger event acknowledgment12 }13});
Here's a commit showing the changes for task 11.
Next, when any of the player clicks on the Get Question
button, the game should begin for all players in the room. The same trivia question should display in the "Trivia" section for all players.
In the client, send an event called getQuestion
to the server when any player clicks on the Get Question
button:
1const triviaQuestionButton = document.querySelector(".trivia__question-btn");2triviaQuestionButton.addEventListener("click", () => {3 // pass null as the second argument because we're not sending any data to the server4 // alert the error if the server sends back an error5 socket.emit("getQuestion", null, (error) => {6 if (error) return alert(error);7 });8});
You will be using trivia questions from the Open Trivia Database API. Here's an example of a JSON response from the API.
As the players interact with the game, we'd have to keep track of different pieces of data, including storing the current question prompt and correct answer, as well as keeping track of how many answers have been submitted.
Let's create helper functions that allow us to manage and update our game data on the server:
1const https = require("https");2const { getAllPlayers } = require("./players.js");3
4// An object containing data for the current game5const game = {6 // prompt keeps track of the current question and answer choices7 // to be displayed to the user8 prompt: {9 answers: "",10 question: "",11 createdAt: "",12 },13 status: {14 submissions: {}, // submissions keeps track of players' answer submissions15 correctAnswer: "", // the correct answer to the current question16 isRoundOver: false, // determines whether or not the answer can be revealed17 },18};19
20// Get information about the game21const getGameStatus = ({ event }) => {22 const { correctAnswer, isRoundOver } = game.status;23
24 if (event === "getAnswer" && isRoundOver) {25 return { correctAnswer };26 }27};28
29// Update the game status30const setGameStatus = ({ event, playerId, answer, room }) => {31 if (event === "sendAnswer") {32 const { submissions } = game.status;33
34 // Store only one response per player per round35 if (!submissions[`${playerId}`]) {36 submissions[`${playerId}`] = answer;37 }38
39 // Set isRoundOver to true when the number of submissions matches the total number of players.40 // Otherwise, set it to false41 game.status.isRoundOver =42 Object.keys(submissions).length === getAllPlayers(room).length;43 }44
45 return game.status;46};47
48// Calls the opentdb API to get a random programming trivia question49const setGame = (callback) => {50 // configure the url query params to get one question (i.e., amount=1)51 // in the programming category (i.e., category=18)52 const url = "https://opentdb.com/api.php?amount=1&category=18";53 let data = "";54
55 // Here, we use the built-in `https` module to make https requests.56 // We need to pass a callback that handles data received in chunks and ends the request.57 const request = https.request(url, (response) => {58 // Listen to the readable stream 'data' event and and process incoming chunks of data59 response.on("data", (chunk) => {60 data = data + chunk.toString();61 });62
63 // When the data stream ends, the stream 'end' event is called once64 response.on("end", () => {65 const { correct_answer, createdAt, incorrect_answers, question } =66 JSON.parse(data).results[0];67
68 game.status.submissions = {};69 game.status.correctAnswer = correct_answer;70 game.prompt = {71 answers: shuffle([correct_answer, ...incorrect_answers]),72 question,73 };74
75 // wrap the data in a callback for asynchronous processing76 callback(game);77 });78 });79
80 // Handle error by logging any error to the server console81 request.on("error", (error) => {82 console.error("An error", error);83 });84 request.end();85};86
87// Shuffles an array. Source: https://javascript.info/task/shuffle88const shuffle = (array) => {89 for (let end = array.length - 1; end > 0; end--) {90 let random = Math.floor(Math.random() * (end + 1));91 [array[end], array[random]] = [array[random], array[end]];92 }93 return array;94};95
96module.exports = {97 getGameStatus,98 setGameStatus,99 setGame,100};
DISCUSS: In your breakout group, discuss what the helper functions in src/utils/game.js
do.
The server will have to listen for the getQuestion
event from the client and invoke setGame
to get a programming trivia question and send it to all players in the room:
1socket.on("getQuestion", (data, callback) => {2 const { error, player } = getPlayer(socket.id);3
4 if (error) return callback(error.message);5
6 if (player) {7 // Pass in a callback function to handle the promise that's returned from the API call8 setGame((game) => {9 // Emit the "question" event to all players in the room10 io.to(player.room).emit("question", {11 playerName: player.playerName,12 ...game.prompt,13 });14 });15 }16});
Make the needed changes in the client to update the Trivia Section with the question prompt. Make sure the Get Question
button is disabled after a question is displayed, as questions cannot be skipped by trying to get a new question.
In the client, listen for the question
event and update the page accordingly with the question prompt.
1// We'll use this helper function to decode any HTML-encoded2// strings in the trivia questions3// e.g., "According to DeMorgan's Theorem, the Boolean expression (AB)' is equivalent to:"4const decodeHTMLEntities = (text) => {5 const textArea = document.createElement("textarea");6 textArea.innerHTML = text;7 return textArea.value;8};9
10socket.on("question", ({ answers, createdAt, playerName, question }) => {11 const triviaForm = document.querySelector(".trivia__form");12 const triviaQuestion = document.querySelector(".trivia__question");13 const triviaAnswers = document.querySelector(".trivia__answers");14 const triviaQuestionButton = document.querySelector(".trivia__question-btn");15 const triviaFormSubmitButton = triviaForm.querySelector(16 ".trivia__submit-btn"17 );18
19 const questionTemplate = document.querySelector(20 "#trivia-question-template"21 ).innerHTML;22
23 // Clear out any question and answers from the previous round24 triviaQuestion.innerHTML = "";25 triviaAnswers.innerHTML = "";26
27 // Disable the Get Question button to prevent the player from trying to skip a question28 triviaQuestionButton.setAttribute("disabled", "disabled");29
30 // Enable the submit button to allow the player to submit an answer31 triviaFormSubmitButton.removeAttribute("disabled");32
33 const template = Handlebars.compile(questionTemplate);34
35 const html = template({36 playerName,37 createdAt: moment(createdAt).format("h:mm a"),38 question: decodeHTMLEntities(question),39 answers,40 });41
42 triviaQuestion.insertAdjacentHTML("beforeend", html);43});
Try clicking on the Get Question
button on any of the players' tab. The same trivia question should appear in all tabs! You won't be able to submit an answer just yet.
Here's a commit showing the changes for this refactoring exercise.
As you might have noticed, using the https
module requires a significant amount of code to get it working.
node-fetch
is a Node.js module that allows the server to use the Fetch
API. Its syntax is much simpler. Let's refactor the setGame
function in src/utils/game.js
to use node-fetch
.
Here's the documentation for your reference. Don't forget to install and save the dependency to your package.json
file!
1$ npm install --save node-fetch
Note how much cleaner the syntax is!
Here's a commit showing the changes for task 12.
In the client, the trivia form should listen for a submit
event. On submit
, send the player's answer to the server.
Don't forget to disable the button after the player has submitted an answer, since we don't want to allow players to submit multiple answers.
1const triviaForm = document.querySelector(".trivia__form");2triviaForm.addEventListener("submit", (event) => {3 event.preventDefault();4
5 const triviaFormSubmitButton = triviaForm.querySelector(6 ".trivia__submit-btn"7 );8 const triviaFormInputAnswer = triviaForm.querySelector(".trivia__answer");9
10 triviaFormSubmitButton.setAttribute("disabled", "disabled");11
12 const answer = event.target.elements.answer.value;13 socket.emit("sendAnswer", answer, (error) => {14 triviaFormInputAnswer.value = "";15 triviaFormInputAnswer.focus();16
17 if (error) return alert(error.message);18 });19});
In the server, listen for and handle the sendAnswer
event:
1// import setGameStatus function2const { setGame, setGameStatus } = require("./utils/game.js");3
4socket.on("sendAnswer", (answer, callback) => {5 const { error, player } = getPlayer(socket.id);6
7 if (error) return callback(error.message);8
9 if (player) {10 const { isRoundOver } = setGameStatus({11 event: "sendAnswer",12 playerId: player.id,13 room: player.room,14 });15
16 // Since we want to show the player's submission to the rest of the players,17 // we have to emit an event (`answer`) to all the players in the room along18 // with the player's answer and `isRoundOver`.19 io.to(player.room).emit("answer", {20 ...formatMessage(player.playerName, answer),21 isRoundOver,22 });23
24 callback();25 }26});
In the client, listen for the answer
event and update the Trivia section with the data from the server:
1socket.on("answer", ({ playerName, isRoundOver, createdAt, text }) => {2 const triviaAnswers = document.querySelector(".trivia__answers");3 const triviaRevealAnswerButton = document.querySelector(4 ".trivia__answer-btn"5 );6
7 const template = Handlebars.compile(messageTemplate);8
9 const html = template({10 playerName: playerName,11 text,12 createdAt: moment(createdAt).format("h:mm a"),13 });14
15 triviaAnswers.insertAdjacentHTML("afterBegin", html);16
17 // If isRoundOver is set to true, activate the reveal answer button18 if (isRoundOver) {19 triviaRevealAnswerButton.removeAttribute("disabled");20 }21});
Here's a commit showing the changes for task 13.
Go ahead and allow any of the players to reveal the answer when the round is over (i.e., when each player has submitted an answer). Use correctAnswer
as your event name.
In the client, emit a getAnswer
event to the server:
1const triviaRevealAnswerButton = document.querySelector(".trivia__answer-btn");2triviaRevealAnswerButton.addEventListener("click", () => {3 socket.emit("getAnswer", null, (error) => {4 if (error) return alert(error);5 });6});
In the server, emit correctAnswer
to all players along with the correct answer:
1socket.on("getAnswer", (data, callback) => {2 const { error, player } = getPlayer(socket.id);3
4 if (error) return callback(error.message);5
6 if (player) {7 const { correctAnswer } = getGameStatus({8 event: "getAnswer",9 });10 io.to(player.room).emit(11 "correctAnswer",12 formatMessage(player.playerName, correctAnswer)13 );14 }15});
Back in the client, listen for the correctAnswer
event and display the correct answer in the trivia section.
1socket.on("correctAnswer", ({ text }) => {2 const triviaAnswers = document.querySelector(".trivia__answers");3 const triviaQuestionButton = document.querySelector(".trivia__question-btn");4 const triviaRevealAnswerButton = document.querySelector(5 ".trivia__answer-btn"6 );7 const triviaFormSubmitButton = triviaForm.querySelector(8 ".trivia__submit-btn"9 );10
11 const answerTemplate = document.querySelector(12 "#trivia-answer-template"13 ).innerHTML;14 const template = Handlebars.compile(answerTemplate);15
16 const html = template({17 text,18 });19
20 triviaAnswers.insertAdjacentHTML("afterBegin", html);21
22 triviaQuestionButton.removeAttribute("disabled");23 triviaRevealAnswerButton.setAttribute("disabled", "disabled");24 triviaFormSubmitButton.removeAttribute("disabled");25});
And there you have it! Your trivia game should now be fully functional. Congratulations!
Did you finish the tutorial early? Here are a few suggestions on how to take your game to the next level!
Implement support for multiple, concurrent channels. Our game logic currently only works properly for one active channel at a time, even though the players can join multiple channels.
Implement an automated scoring system. Create a scoreboard that keeps track of players' scores. Declare a winner or tie after a certain number of rounds or score is reached in the game.
Set a time limit within which players must submit an answer or they forfeit their round.
Allow players to select rather than type an answer. Validate the answer prior to scoring.
Improve and centralize error handling within the application.
Generalize the game by allowing the players to select a different trivia category besides programming.
Deploy your game to Heroku to play with friends and family!
Add a database to save game sessions and scores!
Anything else you can think of!
Take a moment to reflect on what you’ve learned in this tutorial and answer the following questions:
Explain the differences between AJAX Polling, Long Polling, and WebSockets.
What is Socket.IO? Which of the above communication protocols does Socket.IO rely on?
Explain how events work in Socket.IO.
AJAX polling is a technique applied in AJAX applications where the client opens a connection with the server, and then repeatedly sends HTTP requests to a server at regular intervals (e.g., every 1 second) to get any new information that the server has.
In long polling, the client opens a connection with the server and makes an initial HTTP request but does not expect the server to respond immediately.
WebSocket creates a persistent, bi-directional communication session between the user's browser and a web server.
Socket.IO can be used to build a variety of applications that have real-time communication capabilities, such as streaming applications, document collaboration, and instant messaging. Socket.IO uses WebSocket as a transport when the WebSocket API is available in the browser, falling back on HTTP long polling if it's not.
Socket.IO uses events facilitate data transfer between the client and the server. Every event consists of two sides: the sender and the receiver. If the server is the sender, then the client is the receiver. If the client is the sender, then the server is the receiver.
Congratulations, you've reached the end of the Introduction to Node.js tutorial series!
What did you like or didn't like about this post? Let me know what worked well and what can be improved. Your feedback is much appreciated!