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...

Diagram showing workflow for websocket connections

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:

Creating our Angular project

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 the callWebsocket() 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, the ReconnectingWebsocket 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 component chat 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.