Frontend JavaScript — Connect & Send Messages
This is where the UI comes alive. client.js connects to the server via Socket.IO, joins a room when the user clicks “Join Chat”, renders incoming messages as bubbles, and sends new messages when the user hits Send or Enter.
Create public/client.js with the full code below.
Full client.js
// client.js — Browser-side chat logic
const socket = io(); // Connect to same host (localhost:3000 in dev)
// DOM elements
const loginScreen = document.getElementById('login-screen');
const chatScreen = document.getElementById('chat-screen');
const usernameInput = document.getElementById('username');
const roomInput = document.getElementById('room');
const joinBtn = document.getElementById('join-btn');
const loginError = document.getElementById('login-error');
const roomTitle = document.getElementById('room-title');
const messagesEl = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const typingIndicator = document.getElementById('typing-indicator');
const onlineUsersEl = document.getElementById('online-users');
let currentUsername = '';
let typingTimeout = null;
function showError(msg) {
loginError.textContent = msg;
loginError.classList.remove('hidden');
}
function hideError() {
loginError.classList.add('hidden');
}
function formatTime(isoString) {
return new Date(isoString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function scrollToBottom() {
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function appendMessage(data) {
const row = document.createElement('div');
if (data.type === 'system') {
row.className = 'message-row system';
row.innerHTML = '<div class="bubble">' + data.message + '</div>';
} else {
const isMine = data.username === currentUsername;
row.className = 'message-row ' + (isMine ? 'mine' : 'other');
row.innerHTML =
'<div class="meta">' + (isMine ? 'You' : data.username) + '</div>' +
'<div class="bubble">' + escapeHtml(data.message) + '</div>' +
'<div class="time">' + formatTime(data.timestamp) + '</div>';
}
messagesEl.appendChild(row);
scrollToBottom();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function renderOnlineUsers(users) {
if (!users || users.length === 0) {
onlineUsersEl.innerHTML = '<strong>Online</strong><span>Just you</span>';
return;
}
onlineUsersEl.innerHTML = '<strong>Online (' + users.length + ')</strong><span>' + users.join(', ') + '</span>';
}
// ——— Join room ———
joinBtn.addEventListener('click', function () {
hideError();
const username = usernameInput.value.trim();
const room = roomInput.value.trim().toLowerCase();
if (!username) return showError('Please enter your name.');
if (!room) return showError('Please enter a room name.');
if (username.length < 2) return showError('Name must be at least 2 characters.');
currentUsername = username;
roomTitle.textContent = '#' + room;
loginScreen.classList.add('hidden');
chatScreen.classList.remove('hidden');
messageInput.focus();
socket.emit('join-room', { username: username, room: room });
});
// ——— Send message ———
function sendMessage() {
const text = messageInput.value.trim();
if (!text) return;
socket.emit('send-message', text);
messageInput.value = '';
messageInput.focus();
}
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') sendMessage();
});
// Typing indicator — emit on keypress, debounced
messageInput.addEventListener('input', function () {
socket.emit('typing');
});
// ——— Server events ———
socket.on('receive-message', function (data) {
appendMessage(data);
});
socket.on('user-typing', function (data) {
if (data.username === currentUsername) return;
typingIndicator.textContent = data.username + ' is typing…';
typingIndicator.classList.remove('hidden');
clearTimeout(typingTimeout);
typingTimeout = setTimeout(function () {
typingIndicator.classList.add('hidden');
}, 1500);
});
socket.on('online-users', function (users) {
renderOnlineUsers(users);
});
How the client works — step by step
1. Connect to Socket.IO
const socket = io();
One line — connects to the same host and port as the page (localhost:3000 in dev). No URL needed when client and server share origin.
2. Join room flow
When user clicks Join:
- Validate username (not empty, min 2 chars) and room name
- Hide login screen, show chat screen
socket.emit('join-room', { username, room })— tells server to add user to room
3. Sending messages
socket.emit('send-message', text);
We send only the text — server already knows username and room from the join step.
4. Receiving messages
socket.on('receive-message', function (data) { ... });
Server broadcasts three types via the same event:
type: 'system'— join/leave notifications (centered, italic)type: 'chat'— normal messages with username + timestamp
5. Rendering chat bubbles
appendMessage() creates DOM elements dynamically:
- Compares
data.username === currentUsernameto decide mine vs other styling - Uses
escapeHtml()to prevent XSS if someone sends<script>in chat - Formats time with
toLocaleTimeString() - Calls
scrollToBottom()so newest message is visible
6. Typing indicator (bonus — already included)
On every keystroke in message input, client emits typing. Server relays user-typing to others. We hide the indicator after 1.5 seconds of silence.
7. Online users list
Server emits online-users array. renderOnlineUsers() updates the header sidebar.
Security note for instructors
escapeHtml() is essential. Never use innerHTML = userMessage directly — a student prank can become a script injection lesson you did not plan.
Checkpoint
- ✅
client.jssaved inpublic/ - ✅ Join validates inputs before emitting
- ✅ Send works on button click and Enter key
- ✅ Messages auto-scroll to bottom
Next lesson: run the app and test with two browser tabs!