Introduction
This is the second part of a series of tutorials on building a one-on-one (duologue) chatting application with Django channels and SvelteKit. We will focus on building the app's frontend in this part.
NOTE: I won't delve much into the nitty-gritty of SvelteKit as I only intend to show how one can interact with WebSocket in the browser in SvelteKit. I wrote some other tutorials that talk more about it.
Source code
This tutorial's source code can be accessed here:
Sirneij / chatting
Full-stack private chatting application built using Django, Django Channels, and SvelteKit
chatting
chatting
is a full-stack private chatting application which uses modern technologies such as Python
— Django
and Django channels
— and TypeScript/JavaScript
— SvelteKit
. Its real-time feature utilizes WebSocket
.
recording.mp4
chatting
has backend
and frontend
directories. Contrary to its name, backend
is a classic full-fledged application, not only backend code. Though not refined yet, you can chat and enjoy real-time conversations there as well. frontend
does what it implies. It houses all user-facing codes, written using SvelteKit
and TypeScript
.
Run locally
To locally run the app, clone this repository and then open two terminals. In one terminal, change directory to backend
and in the other, to frontend
. For the frontend
terminal, you can run the development server using npm run dev
:
╭─[Johns-MacBook-Pro] as sirneij in ~/Documents/Devs/chatting/frontend using node v18.11.0 21:37:36
╰──➤ npm run dev
In the backend
terminal, create and activate a virtual…
Implementation
Step 1: Setup a SvelteKit project
In our chatting
folder, create a SvelteKit project by issuing the following command in your terminal:
╭─ sirneij in ~/Documents/Devs/chatting on (main)
╰─(ノ˚Д˚)ノ npm create svelte@latest frontend
From the prompts, I chose a skeleton project and added TypeScript support. Follow the instructions given by the command after your project has successfully been created. I added bootstrap (v5) and fontawesome (v6.2.0) to the frontend. I also added routes/+layout.svelte
and modified routes/+page.svelte
. routes/chat/[username]/+page.svelte
was created as well to house the WebSocket logic. Before then, lib/store/message.store.ts
has the following content:
import type { Message } from '$lib/types/message.interface';
import { writable, type Writable } from 'svelte/store';
const newMessages = () => {
const { subscribe, update, set }: Writable<Array<Message>> = writable([]);
return { subscribe, update, set };
};
const messages = newMessages();
const sendMessage = (message: string, senderUsername: string, socket: WebSocket) => {
if (socket.readyState <= 1) {
socket.send(
JSON.stringify({
message: message,
senderUsername: senderUsername
})
);
}
};
export { messages, sendMessage };
This is a custom writable store that exposes a function sendMessage
which does exactly what its name implies. It was used in routes/chat/[username]/+page.svelte
to send messages to the backend. Let's look at the content of routes/chat/[username]/+page.svelte
:
<script lang="ts">
import type { PageData } from './$types';
import Contact from '$lib/components/Contacts/Contact.svelte';
import { session } from '$lib/store/user.store';
import You from '$lib/components/Message/You.svelte';
import Other from '$lib/components/Message/Other.svelte';
import { messages, sendMessage } from '$lib/store/message.store';
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { BASE_URI_DOMAIN } from '$lib/constants';
import type { Message } from '$lib/types/message.interface';
export let data: PageData;
const fullName = `${JSON.parse(data.context.user_object)[0].fields.first_name} ${
JSON.parse(data.context.user_object)[0].fields.last_name
}`;
let messageInput: string, socket: WebSocket;
if (browser) {
const websocketUrl = `${
$page.url.protocol.split(':')[0] === 'http' ? 'ws' : 'wss'
}://${BASE_URI_DOMAIN}/ws/chat/${JSON.parse(data.context.user_object)[0].pk}/?${
$session.user.pk
}`;
socket = new WebSocket(websocketUrl);
socket.addEventListener('open', () => {
console.log('Connection established!');
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
const messageList: Array<Message> = JSON.parse(data.messages).map((message: any) => {
return {
message: message.fields.message,
thread_name: message.fields.thread_name,
timestamp: message.fields.timestamp,
sender__pk: message.fields.sender__pk,
sender__username: message.fields.sender__username,
sender__last_name: message.fields.sender__last_name,
sender__first_name: message.fields.sender__first_name,
sender__email: message.fields.sender__email,
sender__is_staff: message.fields.sender__is_staff,
sender__is_active: message.fields.sender__is_active,
sender__is_superuser: message.fields.sender__is_superuser
};
});
$messages = messageList;
messageInput = '';
});
}
const handleSendMessage = (event: MouseEvent) => {
event.preventDefault();
sendMessage(messageInput, $session.user.username as string, socket);
};
</script>
<div class="container py-5">
<div class="row">
<div class="col-md-6 col-lg-5 col-xl-4 mb-4 mb-md-0 scrollable">
<h5 class="font-weight-bold mb-3 text-center text-lg-start">
{$session.user.username}'s contacts
</h5>
<Contact contacts={JSON.parse(data.context.users)} />
</div>
<div class="col-md-6 col-lg-7 col-xl-8 scrollable" id="message-wrapper">
<h5 class="font-weight-bold mb-3 text-center text-lg-start">
{fullName}
</h5>
<ul class="list-unstyled" id="chat-body">
{#each $messages as message, id}
{#if message.sender__pk === $session.user.pk}
<You {message} />
{:else}
<Other {message} />
{/if}
{/each}
</ul>
<div
class="text-muted d-flex justify-content-start align-items-center pe-3 pt-3 mt-2 message-control"
>
<img
src="https://mdbcdn.b-cdn.net/img/Photos/Avatars/avatar-{$session.user.pk}.webp"
alt="You"
title="You"
style="width: 40px; height: 100%"
/>
<textarea
placeholder="Type message"
class="form-control form-control-lg"
id="message-body"
rows="1"
bind:value={messageInput}
/>
<a
class="ms-3"
id="send-message-btn"
title="Send"
href={null}
on:click={(event) => handleSendMessage(event)}
>
<i class="fas fa-paper-plane" />
</a>
</div>
</div>
</div>
</div>
handleSendMessage
gets fired whenever a user sends a message. The only thing it does is use the exposed sendMessage
function to send the message to the backend. sendMessage
takes, among others, the WebSocket. It was initialized with let socket: WebSocket;
which was populated with:
...
if (browser) {
const websocketUrl = `${
$page.url.protocol.split(':')[0] === 'http' ? 'ws' : 'wss'
}://${BASE_URI_DOMAIN}/ws/chat/${JSON.parse(data.context.user_object)[0].pk}/?${
$session.user.pk
}`;
socket = new WebSocket(websocketUrl);
socket.addEventListener('open', () => {
console.log('Connection established!');
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
const messageList: Array<Message> = JSON.parse(data.messages).map((message: any) => {
return {
message: message.fields.message,
thread_name: message.fields.thread_name,
timestamp: message.fields.timestamp,
sender__pk: message.fields.sender__pk,
sender__username: message.fields.sender__username,
sender__last_name: message.fields.sender__last_name,
sender__first_name: message.fields.sender__first_name,
sender__email: message.fields.sender__email,
sender__is_staff: message.fields.sender__is_staff,
sender__is_active: message.fields.sender__is_active,
sender__is_superuser: message.fields.sender__is_superuser
};
});
$messages = messageList;
messageInput = '';
});
}
...
It must be in the browser
block since SvelteKit does Server-side rendering by default which can be turned off but since we ain't turning it off, we must ensure WebSocket is initialized only in the browser since it's a browser-based API.
With this, we are done! Ensure you take a look at the complete code on GitHub.
Outro
Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and Twitter.
If you found this article valuable, consider sharing it with your network to help spread the knowledge!
Top comments (0)