Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to build a chat app in Angular

Building a chat app in Angular demonstrates real-time communication with WebSockets, infinite scroll for message history, and reactive state management for incoming messages. As the creator of CoreUI with Angular development experience since 2014, I’ve built the chat interface components in CoreUI Angular templates that handle message threading, timestamps, and auto-scroll to the latest message. The architecture uses a WebSocket service that manages the connection and exposes an observable of incoming messages, keeping the chat component clean and focused on rendering. Proper cleanup of WebSocket connections on component destroy prevents memory leaks in long-running applications.

Create a WebSocket chat service.

// chat.service.ts
import { Injectable, OnDestroy } from '@angular/core'
import { Subject, Observable } from 'rxjs'
import { AuthService } from './auth.service'

export interface ChatMessage {
  id: string
  senderId: number
  senderName: string
  text: string
  timestamp: Date
}

@Injectable({ providedIn: 'root' })
export class ChatService implements OnDestroy {
  private ws: WebSocket | null = null
  private messages$ = new Subject<ChatMessage>()
  private status$ = new Subject<string>()

  readonly messages: Observable<ChatMessage> = this.messages$.asObservable()
  readonly status: Observable<string> = this.status$.asObservable()

  constructor(private auth: AuthService) {}

  connect(roomId: string): void {
    const token = this.auth.getToken()
    this.ws = new WebSocket(`${location.origin.replace('http', 'ws')}/ws/chat/${roomId}?token=${token}`)

    this.ws.onopen = () => this.status$.next('connected')
    this.ws.onclose = () => this.status$.next('disconnected')
    this.ws.onerror = () => this.status$.next('error')

    this.ws.onmessage = (event) => {
      try {
        const message: ChatMessage = JSON.parse(event.data)
        this.messages$.next({
          ...message,
          timestamp: new Date(message.timestamp)
        })
      } catch {
        console.error('Invalid message format')
      }
    }
  }

  sendMessage(text: string): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type: 'message', text }))
    }
  }

  disconnect(): void {
    this.ws?.close()
    this.ws = null
  }

  ngOnDestroy(): void {
    this.disconnect()
  }
}

Using Subject instead of BehaviorSubject means new subscribers only receive future messages, not historical ones — which is the correct behavior for a live chat stream. Past messages should be loaded separately from an HTTP endpoint.

Chat Room Component

Display messages and send new ones.

// chat-room.component.ts
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms'
import { ActivatedRoute } from '@angular/router'
import { CardModule, FormModule, ButtonModule } from '@coreui/angular'
import { Subscription } from 'rxjs'
import { ChatService, ChatMessage } from './chat.service'
import { AuthService } from '../auth/auth.service'

@Component({
  selector: 'app-chat-room',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule, CardModule, FormModule, ButtonModule],
  template: `
    <c-card style="height: 80vh; display: flex; flex-direction: column">
      <c-card-header>{{ roomId }}</c-card-header>

      <div #messageContainer class="flex-grow-1 overflow-auto p-3">
        <div *ngFor="let msg of messages"
          [class.text-end]="msg.senderId === currentUserId"
          class="mb-2"
        >
          <div [class]="msg.senderId === currentUserId ? 'bg-primary text-white' : 'bg-light'"
            class="d-inline-block rounded p-2" style="max-width: 75%"
          >
            <small class="d-block fw-bold">{{ msg.senderName }}</small>
            {{ msg.text }}
            <small class="d-block text-end opacity-75">{{ msg.timestamp | date:'HH:mm' }}</small>
          </div>
        </div>
      </div>

      <c-card-footer>
        <form class="d-flex gap-2" (ngSubmit)="send()">
          <input cFormControl [formControl]="messageControl" placeholder="Type a message..." />
          <button cButton color="primary" type="submit" [disabled]="!connected">Send</button>
        </form>
      </c-card-footer>
    </c-card>
  `
})
export class ChatRoomComponent implements OnInit, OnDestroy, AfterViewChecked {
  @ViewChild('messageContainer') private container!: ElementRef

  messages: ChatMessage[] = []
  connected = false
  roomId = ''
  currentUserId = 0

  messageControl = new FormControl('', Validators.required)
  private sub = new Subscription()

  constructor(
    private chat: ChatService,
    private auth: AuthService,
    private route: ActivatedRoute
  ) {}

  ngOnInit(): void {
    this.roomId = this.route.snapshot.params['roomId']
    this.currentUserId = this.auth.getCurrentUser().id

    this.chat.connect(this.roomId)

    this.sub.add(this.chat.messages.subscribe(msg => {
      this.messages.push(msg)
    }))

    this.sub.add(this.chat.status.subscribe(status => {
      this.connected = status === 'connected'
    }))
  }

  ngAfterViewChecked(): void {
    // Auto-scroll to bottom on new messages
    const el = this.container?.nativeElement
    if (el) el.scrollTop = el.scrollHeight
  }

  send(): void {
    if (this.messageControl.valid && this.messageControl.value) {
      this.chat.sendMessage(this.messageControl.value)
      this.messageControl.reset()
    }
  }

  ngOnDestroy(): void {
    this.sub.unsubscribe()
    this.chat.disconnect()
  }
}

ngAfterViewChecked auto-scrolls to the bottom after each render cycle when new messages arrive. Subscription.add() registers multiple subscriptions so they all unsubscribe together in ngOnDestroy.

Best Practice Note

This is the same chat component structure used in CoreUI Angular templates. For production, load message history from a REST endpoint on component init, then switch to WebSockets for live messages. Add optimistic rendering — display the sent message immediately before the server echoes it back, then deduplicate when the echo arrives. For scaling to many concurrent connections, use Socket.IO with Redis adapter to distribute messages across multiple Node.js instances.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to Center a Button in CSS
How to Center a Button in CSS

How to get element ID in JavaScript
How to get element ID in JavaScript

How to Conditionally Add a Property to an Object in JavaScript
How to Conditionally Add a Property to an Object in JavaScript

How to Achieve Perfectly Rounded Corners in CSS
How to Achieve Perfectly Rounded Corners in CSS

Answers by CoreUI Core Team