Frontend: One-on-one (Duologue) chatting application with Django channels and SvelteKit
Tags: webdev, svelte, typescript, tutorial
Published: 2023-01-07 20:13:21

## Introduction

This is the second part of a series of tutorial 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:

{% github  Sirneij/chatting  %}

## Implementation

### Step 1: Setup a SvelteKit project

In our `chatting` folder, create a SvelteKit project by issuing the following command in  your terminal:

```shell
╭─ sirneij in ~/Documents/Devs/chatting on (main)
╰─(ﾉ˚Д˚)ﾉ npm create svelte@latest frontend
```

From the prompts, I chose skeleton project and added TypeScript support. Follow the instructions given by the command after your project has successfully being created. I added [bootstrap (v5)][1] and [fontawesome (v6.2.0)][2] 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:

```typescript
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 message to the backend. Let's look at the content of `routes/chat/[username]/+page.svelte`:

```html
<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 to use the exposed `sendMessage` function to send the message to tte backend. `sendMessage` takes, among others, the websocket. It was initialized with `let socket: WebSocket;` which was populated with:

```typescript
...
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? Consider [contacting me for a job, something worthwhile or buying a coffee ☕](mailto:sirneij@gmail.com). You can also connect with/follow me on [LinkedIn](https://www.linkedin.com/in/idogun-john-nelson/). It isn't bad if you help share it for wider coverage. I will appreciate...

[1]: https://getbootstrap.com/docs/5.0/getting-started/introduction/ "Bootstrap v5.0."
[2]: https://fontawesome.com/ "Fontawesome v6.2.0"