At this stage, my application’s foundation is complete. I can continue to improve existing functionality or build new features. Before the application is released to production and made available for everyone to use, Confetti Cuisine asked me to add an interesting feature to engage users. Without hesitation, I tell them that this is a perfect opportunity to build a chat feature within their Node.js application. Because I don’t want to complicate the application too much before deployment, I’ll keep the chat simple.
The chat will allow only users with accounts to communicate with one another. Every time a message is sent, I’ll save the message and associate it with the sender behind the scenes. Also, I’ll take advantage of socket.io to maintain an open connection between connected clients and the server for real-time communication. Through this library’s event-driven tools, I can emit events from the server to individual clients or all clients and from the client to the server. I could also emit events to a select group of clients, but I won’t need to implement that feature for this application.
Later, I’ll connect a chat icon in the navigation bar to animate whenever a chat message is sent. All users see this icon animate whenever a message is emitted. This icon doubles as a link to the chat page. It’s time to put the finishing touches on the Confetti Cuisine application.
First, I need to install the socket.io package. socket.io offers a JavaScript library that helps me build a real-time communication portal through its use of web sockets and long polling to maintain open connections between the client and the server. To install this package as a dependency, I run npm i socket.io -S in my project’s terminal window.
With this package installed, I need to require it in my main application file and on the client side.
Before I require socket.io, I need to save the server instance I’m creating with Express.js by assigning my app.listen line in main.js to a constant called server. Below this line, I’ll require socket.io in my project by adding const io = require("socket.io")(server). In this line, I’m simultaneously requiring the socket.io module and passing it the instance of my HTTP server used by Express.js. This way, the connection used by socket.io will share the same HTTP server as my main application. With my socket.io instance stored in the io constant, I can start using io to build out my chat functionality.
First, I set up a new controller for chat functionality. Though all the socket.io code can exist in main.js, it’s easier to read and maintain in its own controller. I start by requiring a new controller in main.js and passing it the io object by adding const chatController = require("./controllers/chatController")( io ) to the bottom of main.js. Next, I create chatController.js in my controllers folder. In this file, I add the code from listing 33.1.
I use the same io object created in main.js to listen for specific socket events. io.on ("connection") reacts when a new client connects to my socket server. client.on ("disconnect") reacts when a connected client disconnects. client.on("message") reacts when a client socket sends a custom message event to the server. I can name this event whatever I want. Because I’m working with chat messages, this event name seems to be appropriate. Within that last block, I use io.emit to send a message event back to all connected clients with the same data I received from an individual client. This way, everyone gets the same message that a single user submits.
module.exports = io => { 1 io.on("connection", client => { 2 console.log("new connection"); client.on("disconnect", () => { 3 console.log("user disconnected"); }); client.on("message", (data) => { 4 let messageAttributes = { content: data.content, userName: data.userName, user: data.userId }; io.emit("message"); 5 }); }); };
The last line of code sends a specific set of message attributes that I expect to receive from the client. That is, I expect the client to emit a message event along with content, user name, and user ID. I need to send those three attributes from the view.
To build a successful chat connection, I need a view that facilitates the socket connection from the client side. I want to build my chat box in a view called chat.ejs that’s reachable at the /chat URL path. I create a new route for this path in my homeRoutes.js by adding router.get("/chat", homeController.chat).
Then I add the controller action to match this route by adding the code in the next listing to homeController.js. This code renders my chat.ejs view.
chat: (req, res) => { res.render("chat"); 1 }
To render my chat view, I need to build the view. I create a new file in my views folder called chat.ejs and add the code in listing 33.3. In this Embedded JavaScript (EJS) code, I first check for a currentUser in the view. Earlier, I set up the currentUser as a local variable to reflect an active user session through Passport.js. If a user is logged in, I display the chat form. The form contains three inputs. Two of the inputs are hidden but carry the user’s name and ID. I’ll use these inputs later to send the identity of the message author to the server. The first input is for the actual message content. Later, I’ll grab the value of this input as the content that I submit to the server.
<% if (currentUser) { %> 1 <h1>Chat</h1> <form id="chatForm"> <input id="chat-input" type="text"> <input id="chat-user-id" type="hidden" value="<%= currentUser._id %>"> <input id="chat-user-name" type="hidden" value="<%= currentUser.fullName %>"> 2 <input type="submit" value="Send"> </form> <div id="chat"></div> <% } %>
The last pieces of this puzzle are adding some client-side JavaScript to monitor user interaction on this chat page and submitting the socket.io events needed to notify the server of new messages. In my public folder, I locate confettiCuisine.js and add to it the code in listing 33.4. In this code, I import socket.io for the client and add logic to interact over web sockets with my server. In the first code block, I use jQuery to handle my form’s submission and grab all the values from my form’s three inputs. I expect to receive these same three attributes in my server’s client.on("message") event handler.
The second block of code uses the socket object to represent the specific client on which this code will run. socket.on("message") sets up the client to listen for the message event, which emits from the server. When that event is emitted, each client takes the message delivered with that event and passes it to a custom displayMessage function that I created. This function locates my chat box in the view and prepends the message to the screen.
const socket = io(); 1 $("#chatForm").submit(() => { 2 let text = $("#chat-input").val(), userName = $("#chat-user-name").val(), userId = $("#chat-user-id").val(); socket.emit("message", { content: text, userName: userName, userId: userId }); 3 $("#chat-input").val(""); return false; }); socket.on("message", (message) => { 4 displayMessage(message); }); let displayMessage = (message) => { 5 $("#chat").prepend( $("<li>").html(message.content)); };
Before my application can use the io object in this file, I need to require it within my layout.ejs by adding the following script tag above my confettiCuisine.js import line: <script src="/socket.io/socket.io.js"></script>. This line loads socket.io for the client from my node_modules folder.
I’m ready to launch my application and see chat messages stream from one user to the next. With some styling, I can make it easier for users to distinguish their messages from others. I can also use the user’s name in the chat box so the sender’s name and message appear side by side. To do so, I modify my displayMessage function to print the user’s name, as shown in the next listing. I check whether the message being displayed belongs to that user by comparing the current user’s ID with the ID in the message object.
let displayMessage = (message) => { $("#chat").prepend( $("<li>").html(` <div class='message ${getCurrentUserClass(message.user)}'> <span class="user-name"> ${message.userName}: </span> ${message.content} </div> `)); 1 }; let getCurrentUserClass = (id) => { let userId = $("#chat-user-id").val(); if (userId === id) return "current-user"; 2 else return ""; };
Next, I need to preserve these messages in my database by creating a Message model.
To ensure that my chat feature is worth using and a practical tool for users on the Confetti Cuisine application, the messages can’t disappear every time a user refreshes the page. To fix this problem, I’ll build a Message model to contain the message attributes in the chat form. I create a new message.js file in my project’s models folder and add the code in listing 33.6 to that file.
In this code, I’m defining a message schema that contains content, userName, and user properties. The content of the chat message is required, as are the user’s name and ID. In essence, every message needs some text and an author. If someone tries to save a message somehow without logging in and authenticating, the database won’t allow the data to save. I also set timestamps to true so that I can keep track of when the chat message was added to the database. This feature allows me to show the timestamp in the chat box, if I want.
const mongoose = require("mongoose"), { Schema } = require("mongoose"); const messageSchema = new Schema({ content: { type: String, required: true }, 1 userName: { type: String, required: true }, 2 user: { type: Schema.Types.ObjectId, ref: "User", required: true } 3 }, { timestamps: true }); 4 module.exports = mongoose.model("Message", messageSchema);
This Mongoose model is ready for use in my chat controller. Effectively, when a new message arrives in my chat controller, I attempt to save it and then emit it to other users’ chats. I require this new model in chatController.js by adding const Message = require ("../models/message") to the top of the file. The code in my chatController.js block for client.on("message") is shown in listing 33.7. I start by using the same message-Attributes from earlier in the controller to create a new Message instance. Then I try to save that message. If the message saves successfully, I emit it to all connected sockets; otherwise, I log the error, and the message never gets sent out from the server.
client.on("message", (data) => { let messageAttributes = { content: data.content, userName: data.userName, user: data.userId }, m = new Message(messageAttributes); 1 m.save() 2 .then(() => { io.emit("message", messageAttributes); 3 }) .catch(error => console.log(`error: ${error.message}`)); });
This code allows messages to save to my database, but chat message history still doesn’t appear for users who are connecting for the first time. I’ll correct that problem by loading older messages into my database.
The second task in preserving messages in the chat box is maintaining a consistent number of messages from the chat’s history in the chat box. I decide to allow the chat box to contain the ten most recent chats at any given moment. To do so, I need to load those ten most recent chats from my database and emit them to every client as soon as they connect to the chat.
Within chatController.js, I add the code in listing 33.8 to find the ten most recent chat messages and emit them with a new custom event. I use sort({createdAt: -1}) to sort my database results in descending order. Then I append limit(10) to limit those results to the ten most recent. By emitting the custom "load all messages" event on the client socket, only newly connected users will have their chat boxes refresh with the latest chat messages. Then, I reverse the list of messages with messages.reverse() so that I can prepend them in the view.
Message.find({}) .sort({ createdAt: -1 }) .limit(10) .then(messages => { 1 client.emit("load all messages", messages.reverse()); 2 });
To handle the "load all messages" event on the client side, I add the event handler in the next listing to confettiCuisine.js. In this block of code, I listen for the "load all messages" event to occur. When it does emit, I cycle through the messages received on the client and individually display them in the chat box through the displayMessage function.
socket.on("load all messages", (data) => { 1 data.forEach(message => { displayMessage(message); 2 }); });
The chat is finally complete and ready to test locally. To mimic two separate users communicating, I relaunch my application and log in on two separate web browsers. I navigate to the chat page and see that my chats are being sent in real time over my Node.js application with socket.io.
I want to make one final addition to this application: an icon that lets users elsewhere in the application know when the chat is active. I can easily add this feature with the existing socket.io event set up. All I need to do is add an icon to the navigation bar in my application by adding <a href="/chat" class="chat-icon">@</a> to layout.ejs. With this line alone, I have an icon in my navigation bar that links to the /chat route.
Next, I animate the icon by having it flash twice whenever a chat message is sent. Because I’m already emitting the message event from the server every time a new message is submitted, I can add the icon animation to the client’s handler for that event.
In confettiCuisine.js, I modify the socket.on("message") code block to look like the code in the following listing. In this code, I display the message in the chat box as usual and additionally target an element with the chat-icon class. This element represents my chat icon in the navigation bar. Then I rapidly fade the icon out and back in, twice.
socket.on("message", (message) => { displayMessage(message); for (let i = 0; i < 2; i++) { $(".chat-icon").fadeOut(200).fadeIn(200); 1 } });
With this extra feature, users have some indication that conversations are taking place on the chat page.
I could add to this chat feature in plenty of ways. I could create separate chats for each Confetti Cuisine class, for example, or use socket.io events to notify users when they’ve been tagged in a chat. I’ll consider implementing these features in the future.
In this capstone exercise, I added a real-time chat feature to my Confetti Cuisine application. I used socket.io to simplify connections between the server and multiple clients. I used some built-in and custom events to transfer data between open sockets. At the end, I added a feature to notify users who aren’t in the chat room that others are actively communicating. With this feature added, I’m ready to deploy the application.
3.147.80.3