Skip to content

Angular in depth

The Chat libraries ship Angular adapters that give you Observables, an injection token and Signals. This guide wires them into an app. For a first working result, use the Angular quickstart.

Provide one Chat client through dependency injection, read data as Signals, and connect live updates that clean up after themselves.

provideChatUserClient takes a factory that returns the client options. The factory runs in an injection context, so it can inject() your own services, for example one that knows the current tenant. Register it in app.config.ts, or in a route’s or component’s providers when the tenant or application changes there.

app.config.ts
import { inject, Injectable, type ApplicationConfig } from '@angular/core';
import { provideChatUserClient } from '@central-code-station/ccs-chat-user-client/angular';
import { getChatAccessToken } from './fe-guide-chat-token';
@Injectable({ providedIn: 'root' })
export class TenantService {
readonly tenantId = 'fitness'; // in a multi-tenant app: from the signed-in user
}
export const appConfig: ApplicationConfig = {
providers: [
provideChatUserClient(() => ({
chatApiUrl: 'https://chat.centralcodestation.com/api/v1',
tenantId: inject(TenantService).tenantId, // the factory runs in an injection context
applicationId: 'fitness-app',
getAccessToken: getChatAccessToken,
})),
],
};

Every method of the injected client returns a cold Observable, like HttpClient. Nothing is sent until you subscribe, and each subscription sends the request again. toSignal subscribes for you.

conversations.component.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { CHAT_USER_CLIENT } from '@central-code-station/ccs-chat-user-client/angular';
@Component({
selector: 'app-conversations',
standalone: true,
template: `
@for (conversation of conversations()?.items; track conversation.conversation_id) {
<button (click)="send(conversation.conversation_id)">{{ conversation.name ?? conversation.type }}</button>
}
`,
})
export class ConversationsComponent {
private readonly chat = inject(CHAT_USER_CLIENT);
// Cold Observable: the request is sent when toSignal subscribes.
readonly conversations = toSignal(this.chat.conversations.list({ limit: 30 }));
send(conversationId: string): void {
this.chat.messages.send(conversationId, { body: 'See you at 9!' }).subscribe();
}
}

The promise-based ChatUserClient is available as chat.client. Outside DI, createChatUserClient(options) builds the same wrapper.

createChatNotifications is a plain factory function. Wrap it in your own @Injectable() service and pass inject(DestroyRef) so it disconnects when the service is destroyed. It exposes connectionState as a Signal and on$(event) as an Observable for each event.

chat-live-updates.ts
import { DestroyRef, inject, Injectable } from '@angular/core';
import { createChatNotifications } from '@central-code-station/ccs-chat-notifications-client/angular';
import { getChatAccessToken } from './fe-guide-chat-token';
@Injectable({ providedIn: 'root' })
export class ChatLiveUpdates {
private readonly notifications = createChatNotifications(
{
chatApiUrl: 'https://chat.centralcodestation.com/api/v1',
tenantId: 'fitness',
applicationId: 'fitness-app',
getAccessToken: getChatAccessToken,
},
inject(DestroyRef), // disconnects when this service's injector is destroyed
);
readonly connectionState = this.notifications.connectionState; // Signal<ConnectionState>
readonly messageCreated$ = this.notifications.on$('message.created');
readonly typing$ = this.notifications.on$('typing');
connect(): void {
this.notifications.connect();
}
viewPublicChannel(conversationId: string): void {
this.notifications.subscribeToConversation(conversationId);
}
}

Why factory functions and not @Injectable classes

Section titled “Why factory functions and not @Injectable classes”

The libraries ship plain compiled JavaScript, with no Angular compiler pass. A decorated class in the package would have no Ivy metadata and would fail dependency injection at runtime. A provider function, an InjectionToken and a factory work without it. Your own @Injectable() wrapper, compiled by your build, is fine.

Start the app. The component lists your conversations. Send a message from another user and messageCreated$ emits. connectionState() reads connected.

You see Cause Fix
NG0203 inject(DestroyRef) ran outside an injection context. Create the notifications object in a field initializer or constructor, or pass a DestroyRef you already hold.
No request is sent Nobody subscribed to the Observable. Subscribe, or use toSignal or the async pipe.
Duplicate requests Two subscriptions to the same cold Observable. Subscribe once and share the result.
Live events stop after navigation The service is provided in a route’s injector, which was destroyed. Provide it in root.