- Published on
Guide to using Websockets in Angular Part 1
Ever wanted to develop a chat application using Angular? Ever though about how chat applications handle and organize messages that are sent from multiple users at different locations at different times? Well the answer to all these questions are simple: Websockets.
In this blog post, we will learn how to implement Websockets in Angular. Before learning how to implement WebSockets let's go over a brief explanation of a Websocket actually is...
Figure 1: Diagram showing workflow for websocket connections
Websocket Definition
A Websocket is a communication protocol, similar to the HTTP protocol, that allows for full-duplex communication. Full duplex communication means two or more parties can simultaneously send and receive data. Therefore, unlike the HTTP protocol which uses half-duplex communication(meaning that any given party can only transfer data in one direction at a time), the Websocket protocol allows for bidirectional communication between the clients and the server.
The terms consumer and producer are used to describe the relationship between the server and it's clients. The producer is the party that initiates the sending of a message through the Websocket, while the consumer is the party that receives that message at the other end of the Websocket. It should be mentioned that the consumer is not necessarily always the server and the producer is not always the client. The roles can be switch very easily, quite unlike the HTTP protocol!
What we will build
We are going to build an Angular chat room application that issues websocket connection requests to a backend application. Note that the implementation of the backend application will not be covered in this blog post series for the sake of brevity. Since we are only concerned with the frontend application, the websocket connections to the backend will fail as expected(as the backend application does not exist yet) and the data for the displaying of chat messages will be supplied by a JSON file.
Hopefully that answers any questions you have regarding the development of this application. :)
Prerequisites
To be specific, we'll be using the reconnecting-websocket
npm package as our Websocket provider. A major benefit of the reconnecting-websocket
is that if a websocket connection attempt were to fail, then it would retry for a successful connection repeatedly for a specified duration. Also, it is important to mention that it uses the JavaScript WebSocket API so it is widely supported on almost all modern browsers.
Before we begin, make sure you have a working Angular project and the npm
package manager installed on your local machine.
Try to use the following versions for the aforementioned technologies:
- Angular 10.x LTS
Now let's begin👷♂️
Setup
First, let's create a folder to host our Angular and Node.js applications. Name it whatever you want and add it to your workspace in your coding IDE(I'm using VSCode).
Now, we can start creating the Angular application by running the command: ng new angularApp
in the terminal. In the following prompts that appear, make sure to select 'Y' for Angular routing and 'CSS' as the stylesheet file type like so:
Figure 2: Creating our Angular project
Next we will need to create the four components inside our Angular project: Chat
, Message
, others-chat
, and my-chat
.
To do this let's run the following commands:
ng generate component chat
ng generate component message
ng generate component others-chat
ng generate component my-chat
Now, you'll notice two folders being created in the src/app
folder.
Next, we need a service class for the Chat component, so cd
into the chat
folder and run the following command:
ng generate service chat
This command automatically creates a ChatService
class in the chat
folder.
Ok, now that we have finished the setup portion of our Angular application, let's move on to the development stage
Development
First, let's start out by creating an environment.ts
file in the src/app/environments
folder. After this, let's add the following code to this file:
export const environment = {
production: false,
WS_URL: 'ws://localhost:8000/'
};
The WS_URL
variable holds the URL to the backend server that we will send websocket messages to via the WS protocol, also known as full-duplex communication. Notice the URL begins with ws
not http
, which indicates the use of the websocket protocol.
Secondly, let's start out by adding the following code to the chat
component's typescript file:
export class ChatComponent implements OnInit {
public messages: ChatMessage[] = new Array<ChatMessage>();
private websocket: ReconnectingWebSocket | null = null;
private chatGroupId: number = 0;
constructor() { }
ngOnInit(): void {
this.callWebsocket();
}
callWebsocket(){
this.websocket = new ReconnectingWebSocket(
environment.WS_URL + "ws/classroom/" + this.chatGroupId
);
this.websocket.onopen = (evt) => {
console.log("Successfully connected to websocket!");
}
this.websocket.onclose = () => {
console.log("... trying to reconnect websocket in 3 seconds");
setTimeout(this.callWebsocket, 3000);
}
this.websocket.onmessage = (evt) => {
const data = JSON.parse(evt.data);
var chatMessage = new ChatMessage();
chatMessage.content = data.message;
chatMessage.username = data.user;
chatMessage.timestamp = data.timestamp;
chatMessage.the_type = data.the_type;
chatMessage.user_profile_img = data.user_profile_img;
this.messages.push(chatMessage);
}
}
formatDate(dateToFormat : string | undefined) {
if(dateToFormat){
const date = new Date(dateToFormat)
const today = new Date
const yesterday = new Date
let format_date = date.toLocaleDateString(
'en-US',{weekday:'long',month:'long',day:'numeric'}
)
yesterday.setDate(today.getDate() - 1)
if(date.toLocaleDateString() == today.toLocaleDateString()){
format_date = 'Today'
}else if (date.toLocaleDateString() == yesterday.toLocaleDateString()){
format_date = 'Yesterday'
}
return format_date
}
return null;
}
}
Now for an explanation of the above code:
- Upon application start up the
ngOnInit()
lifecyle hook calls thecallWebsocket()
method callWebsocket()
sets up the websocket connection with the backend(the backend websocket implementation using Node.js will be covered in a later blog post)- You'll notice that we are using the
ReconnectingWebsocket
class to serve as the websocket wrapper class. This is done because, unlike the default JavaScript Websocket API, theReconnectingWebsocket
will repeatedly try to re-connect the websocket connection upon being disconnected or in the case of a failed connection attempt. Note: This class must be implemented in a typescript file, which will be covered below - Next we define several websocket connection handler methods, the most notable being
onopen()
because it will be executed once the connection is successfully established. - The
onmessage
handler method is called whenever an end user clicks the Send button in the frontend UI - The
onclose
handler method is run when the the end user exits the chat page in our Angular application - Finally, the
formatDate(dateToFormat : string | undefined)
method takes in a date in string format and transforms it into a more human readable format(if the date less than 24 hours ago then it is transformed to the 'Today' string)
Before we forget, let's add create the ReconnectingWebsocket
class. To do this just navigate to the src/app/services
folder and create a new file called reconnecting-websocket.ts
. For the sake of brevity just visit this GitHub URL to copy and paste the logic for the ReconnectingWebsocket
class.
Now, let's move on to the chat.component.html
file and add the following code to it:
<div *ngFor="let message of messages" class="chat-messages">
<div *ngIf="message.type === 'date'" class="chat-date">
<div>
<span class="chat-date-text">
{{formatDate(message.content)}}
</span>
</div>
</div>
<app-others-chat *ngIf="currentUserId == message.userId" [message]="message"></app-others-chat>
<app-my-chat *ngIf="currentUserId != message.userId" [message]="message"></app-my-chat>
</div>
As you see from the code above, we are using Angular's ngFor
directive to iterate over the list of established messages and render them one by one to the DOM. The choice of rendering either the my-chat
or others
component is decided by the type of message being rendered. That is, if it's from the current user then the my-chat
component is render and if not, the others-chat
component is displayed.
The main difference between the my-chat
and others-chat
messages are that they are in different colors, with the my-chat
messages having a white color and the others-chat
having a grey color.
With the chat component finished, we now have to create the ChatMessage
model class. To do this, first cd
into into the src/app
folder and create a folder called models
. Then cd
into the models
folder and create a file called chat.message.model.ts
. In it, let's add the following code:
export class ChatMessage {
id: string | undefined;
userId: string;
username: string | undefined;
timestamp: number | undefined;
type: string | undefined;
user_profile_img?: string;
edited: boolean | undefined;
content: string | undefined;
}
This ChatMessage
will be used to represent chat messages that are sent and recieved from and to this Angular application. Notice the |
operator is used to give a fallback value of undefined
for the fields in this model. This is because this Angular application is running on strict mode and therefore requires strict static typing procedures.
Let's move on to the others-chat
component now.
In the others-chat
component's typescript file, let's add the following code:
export class OthersChatComponent implements OnInit {
current_user : string | null;
@Input() message: ChatMessage;
constructor(
public chatService: ChatService
){
if(sessionStorage.getItem("username")){
this.current_user = sessionStorage.getItem("username");
}
}
ngOnInit(){
}
}
Now for an explanation of the code above:
- The
current_user
string stores the username of the currently logged in user. This username value is obtained using Angular's sessionStorage feature. SessionStorage is very similar to sessions in traditional server side frameworks such as PHP and Django. To learn more about Angular's sessionStorage please visit this link: https://www.delftstack.com/howto/angular/session-storage-in-angular/ - The
message
string variable is obtained from the parent componentchat
via the@Input
directive. The@Input
directive allows the parent component to pass values to it's child components, as seen in this instance
After this, the template file for the others-chat
component is next so let's add the appropriate code now:
<div class="chat-item-o message-box-holder " >
<div class="other">
<div class="message">
<div class="head">
<img class="chatbox-avatar" src="{{ message.user_profile_img }} " alt="" />
<h4 class="chat-partner-name">{{message?.username}}</h4>
<i>{{ chatService.formatTime(message?.timestamp) }}</i>
</div>
<p class="content message-box">
{{message?.content}}
<span *ngIf="message?.type === 'plain' || message?.type === undefined "
[innerHTML]="message?.content">
</span>
</p>
</div>
</div>
<span *ngIf="message?.edited" class="material-icons-outlined">edited</span>
</div>
What we are doing here is rendering a chat message with the appropriate message content and timestamp. The ChatMessage
model
Now that we're done the others-chat
component, let's code out the my-chat
component next by adding the following code to the my-chat.component.ts
file:
export class MyChatComponent implements OnInit {
current_user : string | null;
@Input() message: ChatMessage;
constructor(
public chatService: ChatService
){
this.current_user = sessionStorage.getItem("username");
}
ngOnInit(): void {}
}
As you see above, the my-chat
component is almost identical to the others-chat
component. This is expected as they both have the shared responsibility of rendering chat messages to the DOM.
Next up is the my-chat.component.html
file:
<div class="chat-item message-box-holder">
<span *ngIf="message.edited"><i-feather name="edit-2" class="edit-2"></i-feather></span>
<div class="my">
<div class="message">
<div class="head">
<img class="chatbox-avatar" src="{{ message.user_profile_img }} " alt="" />
<h4 >{{message.username}}</h4>
<i>{{ chatService.formatTime(message.timestamp) }}</i>
</div>
<p class="content message-box message-partner">
{{message?.content}}
<span *ngIf="message?.type === 'plain' || message?.type === undefined "
[innerHTML]="message?.content">
</span>
</p>
</div>
</div>
<span *ngIf="message?.edited" class="material-icons-outlined">edited</span>
</div>
As previously mentioned, both the others-chat
and the my-chat
components are very similar so it is fitting that they have similar template layouts. Also, note we are using the formatTime()
method in the chatService
to format the chat message timestamps in human-readable datetime values.
For the sake of brevity, that's all we'll cover in this post. Stay tuned for part 2 of this blog series where we continue the development of this Angular application, starting right where we left off here.
Note: If you want to clone/fork this application feel free to visit my GitHub repo for it here.
Conclusion
Well that's it for this post! Thanks for following along in this article and if you have any questions or concerns please feel free to post a comment in this post and I will get back to you when I find the time.
If you found this article helpful please share it and make sure to follow me on Twitter and GitHub, connect with me on LinkedIn, subscribe to my YouTube channel.