When we need to create a chat, managing multiple chats manually can be cumbersome, especially adding features like editing, deleting and favoriting. See how to do this using Kendo UI for Angular with Angular Signals.
Nowadays, with the race in AI, companies want to build a chatbot like Gemini or ChatGPT. And the users don’t just want to chat—they also want to keep their conversations, organize them, prioritize them and return to previous conversations.
As developers, we need to deliver quickly to compete in the market, but when the marketing team wants to create a MVP that covers those features, it’s not always easy. We need to create components like chat, lists that allow editing, trigger actions, all in no time. After feeling overwhelmed with the amount of work, I have good news for you. Progress Kendo UI for Angular offers a set of components that can help you deliver your project faster, and with a great UI.
Today, we’re going to learn how to create a multi-conversation chat easily with Kendo UI for Angular Conversational UI + Angular ListView. Combining these with the power of Angular Signals, we can build a chat with multiple conversations and allow the users to select, edit, delete, mark as favorite, start new chats and keep history of our conversations in no time.
Let’s get started!
I’ll use the latest version of Angular. The easiest way to set up the project is by running the following command in the terminal. Answer the default questions:
npx -p @angular/cli ng new conversation-chat
? Which stylesheet format would you like to use? CSS
Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? no
After that, we’ll add Conversational UI and ListView for Angular in our project.
Move to the project folder cd conversation-chat
and install the Kendo UI for Angular Conversational UI using schematics by running:
ng add @progress/kendo-angular-conversational-ui
Searching for compatible package version
› Found compatible package version: @progress/kendo-angular-conversational-ui@16.10.0.
✔ Loading package information from registry
✔ Confirming installation
✔ Installing package
UPDATE package.json (1666 bytes)
UPDATE angular.json (2865 bytes)
✔ Packages installed successfully.
UPDATE src/main.ts (295 bytes)
UPDATE tsconfig.app.json (455 bytes)
UPDATE tsconfig.spec.json (461 bytes)
UPDATE angular.json (2945 bytes)
After finishing that, install the Angular ListView using schematics with a similar command:
ng add @progress/kendo-angular-listview
✔ Determining Package Manager
› Using package manager: npm
✔ Searching for compatible package version
› Found compatible package version: @progress/kendo-angular-listview@16.10.0.
✔ Loading package information from registry
✔ Confirming installation
✔ Installing package
@progress/kendo-theme-default already installed, skipping styles registration.
UPDATE package.json (1911 bytes)
✔ Packages installed successfully.
If you aren’t already a Kendo UI for Angular customer, you can get a trial license free for 30 days.
Now that we have our Kendo UI components, we need to create a set of interfaces and mock data. Let’s go!
First, we need to define the structure of our conversations and initialize some mock data. Each conversation will have an ID, a name, a fav status and an array of messages, but we don’t need to build everything from zero. We can take advantage of Kendo UI, which provides for us a set of types to make our lives easier.
Create the src/entities/chat-entities.ts
file. In it, define the type for our conversations: id
, name
and fav
. For the Message
and User
, we won’t create a new type; Conversational UI
provides the Message
and User
types, saving us time.
Open the src/entities/chat-entities.ts
, import the Message and User from @progress/kendo-angular-conversational-ui
and create the type ChatConversation
:
import { Message, User } from '@progress/kendo-angular-conversational-ui';
export type ChatConversation = {
id: string;
name: string;
fav: boolean;
active: boolean;
messages: Array<Message>;
};
In our project, we need to create the initialConversation
for our chats.
export const initialConversation: ChatConversation = {
id: crypto.randomUUID(),
name: 'Initial conversation',
messages: [],
active: false,
fav: false,
};
We’re going to create two variables, the defaultUser
and AIBot
, with the User
interface:
export const defaultUser: User = {
id: crypto.randomUUID(),
name: 'Dany',
};
export const AIBot: User = {
id: crypto.randomUUID(),
name: 'Mandy AI',
};
Finally, we need a new empty conversation, but without name and id, because we’re going to create a new conversation using Omit<ChatConversation, 'name' | 'id'>
.
Learn more about Typescript Utility types.
export const firstAIInteraction: Omit<ChatConversation, 'name' | 'id'> = {
active: true,
messages: [
{
author: AIBot,
text: 'Welcome to Kendo AI',
},
],
fav: false,
};
The final code in chat-entities.ts
:
import { Message, User } from '@progress/kendo-angular-conversational-ui';
export type ChatConversation = {
id: string;
name: string;
fav: boolean;
active: boolean;
messages: Array<Message>;
};
export const defaultUser: User = {
id: crypto.randomUUID(),
name: 'Dany',
};
export const AIBot: User = {
id: crypto.randomUUID(),
name: 'Mandy AI',
};
export const initialConversation: ChatConversation = {
id: crypto.randomUUID(),
name: 'Initial conversation',
messages: [],
active: false,
fav: false,
};
export const firstAIInteraction: Omit<ChatConversation, 'name' | 'id'> = {
active: true,
messages: [
{
author: AIBot,
text: 'Welcome to Kendo AI',
},
],
fav: false,
};
We’ve now defined our entities to be used in our chat application. Now, let’s tackle some of the reactive pieces that will pull the functionality together.
First, open the app.component.ts
. There, declare a variable signal currentConversation
of type ChatConversation
, and a signal
array of ChatConversations
to represent the list of conversations and defaultUser.
defaultUser = defaultUser;
currentConversation = signal<ChatConversation>(initialConversation);
conversations = signal<ChatConversation[]>([]);
The final code looks like this in app.component.ts
file:
import { Component, signal } from '@angular/core';
import { ChatConversation } from '../entities/chat-entities';
@Component({
selector: 'app-root',
standalone: true,
imports: [],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
readonly defaultUser = defaultUser;
currentConversation = signal<ChatConversation>(initialConversation);
conversations = signal<ChatConversation[]>([]);
}
In the app.component.ts
, we will handle the actions a user can take: createNewConversation, updateConversation, delete and updateMessages.
First, the method createNewConversation()
. It creates a newConversation
object. To save time, we use the spread operator in the firstAIInteraction
, but by adding the two missing properties, the name and the id, the id is generated by crypto.randomUUID.
createNewConversation() {
const newConversation: ChatConversation = {
...firstAIInteraction,
name: `New Chat ${this.conversations().length + 1}`,
id: crypto.randomUUID(),
};
}
Next, update the conversation’s signal array, add the new conversation and set it as the current conversation.
this.conversations.update((currentItems) => [
...currentItems,
newConversation,
]);
this.currentConversation.set(newConversation);
The final code looks like:
import { ChatConversation, defaultUser, firstAIInteraction, initialConversation } from '../entities/chat-entities';
…
createNewConversation() {
const newConversation: ChatConversation = {
...firstAIInteraction,
name: `New Chat ${this.conversations().length + 1}`,
id: crypto.randomUUID(),
};
this.conversations.update((currentItems) => [
...currentItems,
newConversation,
]);
this.currentConversation.set(newConversation);
}
Next, we need to update the conversation’s array signals when the user makes changes on it. Create a new method updateConversation
:
updateConversation(conversation: ChatConversation) {
this.conversations.update((currentItems) =>
currentItems.map((p) => (p.id === conversation.id ? conversation : p)),
);
}
Let’s call our conversation update method and use map to loop through the conversations, find the one you want to update, and replace the old object with the conversation you passed in. In this case, I am referencing our old conversation as p
.
Next, when the user removes a conversation, we need to delete it from the conversation’s signal array and reset the currentConversation to the initial, if they are the same.
deleteConversation(id: string) {
this.conversations.update((conversations) =>
conversations.filter((p) => p.id !== id),
);
if (id === this.currentConversation().id) {
this.currentConversation.set(initialConversation);
}
}
Finally, we want to listen to the new message in a conversation. So we need to update the currentConversation with the message streamed by the kendo-chat and update the current conversation and the conversations array.
import { SendMessageEvent } from '@progress/kendo-angular-conversational-ui';
…
updateMessage($event: SendMessageEvent) {
this.currentConversation.update((c) => ({
...c,
messages: [...c.messages, $event.message],
}));
this.updateConversation(this.currentConversation());
}
We have the state for current and conversations and methods to handle actions from the user, so it’s time to show the list of conversations and allow the user to perform actions on it. To do this, we’ll create the ConversationList
component. Let’s do it.
The ConversationListComponent
to handle displaying the list of conversations it edit, delete, pin/unpin and start new conversations.
First create the conversation-list
component using Angular/CLI command:
ng g c components/conversation-list
Open conversation-list.component.ts
. Here we need to do some stuff and use the magic of ListViewComponent.
The ListViewComponent helps us easily show a list of items. And, on top of that, it also provides a painless way to edit data , combining with Angular forms (template-driven and reactive). In my case, I will pick the template-driven option, only needing to write a few lines of code to handle the events.
Read more about Angular ListView.
First, we’re going to create two model properties to react to changes automatically when conversations
and conversationSelected
change and two output signals, conversationUpdated
and conversationDeleted
.
conversations = model.required<ChatConversation[]>();
conversationSelected = model.required<ChatConversation>();
conversationUpdated = output<ChatConversation>();
conversationDeleted = output<string>();
Learn more about Signals Model Inputs.
Next, declare two variables for the state in the conversation-list
:
public editedConversation: ChatConversation | null = null;
public editedIndex: number | null = null;
We have the first element, but its event and state need to be handled in our code. So it’s time to handle the events.
Learn more about ListView Edit Mode.
The KendoListView makes it easy to manage the conversation by providing kendoListViewEditTemplate
, kendoListViewRemoveCommand
and kendoListViewEditCommand
directives to manage/edit templates and delete actions. But before we connect the Kendo ListView, first we need to write a bit of code.
app.component
and close the editor.app.component.ts
.First, we create two key actions, resetEdit
to restore the editedConversation
and editedIndex
when a user performs actions, and cancelEdit
with cancelHandler
.
private resetEdit() {
this.editedConversation = null;
this.editedIndex = null;
}
private closeEditor(sender: any, itemIndex = this.editedIndex) {
sender.closeItem(itemIndex);
this.resetEdit();
}
editHandler({ sender, itemIndex, dataItem }: EditEvent) {
this.closeEditor(sender);
this.editedConversation = { ...dataItem };
this.editedIndex = itemIndex;
sender.editItem(itemIndex);
}
cancelHandler({ sender, itemIndex }: CancelEvent) {
this.closeEditor(sender, itemIndex);
}
We have four remaining user actions for which we need to use our output signals: save, remove, togglePin and select.
Add two handlers for saveHandler
and onTogglePin
and use conversationUpdated.emit
to propagate the changes.
saveHandler({ sender, itemIndex, dataItem }: SaveEvent) {
sender.closeItem(itemIndex);
this.resetEdit();
this.conversationUpdated.emit(dataItem);
}
onTogglePin(dataItem: ChatConversation) {
dataItem.fav = !dataItem.fav;
this.conversationUpdated.emit(dataItem);
}
Finally add the removeHandle
to trigger the conversationDeleted
and update the signal’s conversationSelected
in the onConversationSelected
method.
removeHandler({ dataItem }: RemoveEvent) {
this.conversationDeleted.emit(dataItem.id);
}
onConversationSelect(dataItem: ChatConversation) {
dataItem.active = true;
this.conversationSelected.set(dataItem);
}
Perfect! We have all methods ready to use the KendoListView in the conversation-list.component.html
. But before we start, we must import directives and components to simplify the integration.
In the conversations-list.component.ts
, go to the imports section. Add ListViewComponent
, ItemTemplateDirective
, EditCommandDirective
, FormsModule
and ButtonComponent
.
The final version of conversation-list.component.ts
looks like:
import { Component, model, output } from '@angular/core';
import { ChatConversation } from '../../../entities/chat-entities';
import {
CancelEvent,
EditCommandDirective,
EditEvent,
ItemTemplateDirective,
KENDO_LISTVIEW,
ListViewComponent,
RemoveEvent,
SaveEvent,
} from '@progress/kendo-angular-listview';
import { FormsModule } from '@angular/forms';
import { ButtonComponent } from '@progress/kendo-angular-buttons';
@Component({
imports: [
ListViewComponent,
ItemTemplateDirective,
EditCommandDirective,
KENDO_LISTVIEW,
FormsModule,
ButtonComponent,
],
selector: 'app-conversations-list',
standalone: true,
templateUrl: './conversation-list.component.html',
})
export class ConversationsListComponent {
conversations = model.required<ChatConversation[]>();
conversationSelected = model.required<ChatConversation>();
conversationUpdated = output<ChatConversation>();
conversationDeleted = output<string>();
public editedConversation: ChatConversation | null = null;
public editedIndex: number | null = null;
editHandler({ sender, itemIndex, dataItem }: EditEvent) {
this.closeEditor(sender);
this.editedConversation = { ...dataItem };
this.editedIndex = itemIndex;
sender.editItem(itemIndex);
}
cancelHandler({ sender, itemIndex }: CancelEvent) {
this.closeEditor(sender, itemIndex);
}
private resetEdit() {
this.editedConversation = null;
this.editedIndex = null;
}
private closeEditor(sender: any, itemIndex = this.editedIndex) {
sender.closeItem(itemIndex);
this.resetEdit();
}
saveHandler({ sender, itemIndex, dataItem }: SaveEvent) {
sender.closeItem(itemIndex);
this.resetEdit();
this.conversationUpdated.emit(dataItem);
}
onTogglePin(dataItem: ChatConversation) {
dataItem.fav = !dataItem.fav;
this.conversationUpdated.emit(dataItem);
}
removeHandler({ dataItem }: RemoveEvent) {
this.conversationDeleted.emit(dataItem.id);
}
onConversationSelect(dataItem: ChatConversation) {
dataItem.active = true;
this.conversationSelected.set(dataItem);
}
}
We have the events, so it’s time to connect them with the KendoListView.
Open conversation-list.component.html
, where we’ll use the Kendo UI ListView component to display the list of conversations. We’ll also add buttons for editing, deleting and toggling the pin status. When the edit button is enabled, a form will allow users to modify the conversation title.
The magic of Kendo ListView
works with our events. We only need to tell the events and define the template, and KendoListView
takes care of simplifying, showing the list of items and switching between view and edit mode.
The kendo-listview has the data
property to bind the list of conversations and kendoListViewItemTemplate
directive to define the template for displaying each conversation item. You can fully customize how each conversation appears in the list.
To trigger actions, we have kendoListViewEditCommand
to trigger edit mode for a conversation item, allowing users to modify conversation details and kendoListViewRemoveCommand
to handle the deletion of a conversation from the list.
I recommend reading more in the Kendo UI ListView documentation.
We need to bind the handles in each kendo-listview
event by passing the event.
<kendo-listview
[data]="conversations()"
(edit)="editHandler($event)"
(cancel)="cancelHandler($event)"
(save)="saveHandler($event)"
(remove)="removeHandler($event)"
>
</kendo-listview>
Next we add an ng-template
with the kendoListViewItemTemplate
directive. It shows the conversation name. If dataItem.fav
is true
then we show the ❤️.
We add three buttons to trigger our actions. Pin
binds on click the onTogglePin
function and passes the dataItem. The Edit
and Remove
buttons work with kendoListViewEditCommand
and kendoListViewRemoveCommand
. Kendo UI takes the responsibility to switch from edit to read mode automatically and triggers the actions related to them.
<ng-template kendoListViewItemTemplate let-dataItem="dataItem">
<div class="k-d-flex k-justify-content-around k-cursor-pointer">
@if (dataItem.fav) {
<span class="pinned-label">❤️</span>
}
<p (click)="onConversationSelect(dataItem)">{{ dataItem.name }} </p>
<div class="chat-actions">
<button kendoButton (click)="onTogglePin(dataItem)">Pin</button>
<button kendoListViewEditCommand >Edit</button>
<button kendoListViewRemoveCommand >Remove</button>
</div>
</div>
</ng-template>
The form is shown by using another ng-template
with the kendoListViewEditTemplate
directive, along with the input and ngModel
. Instead of using click
, we use the kendoListViewSaveCommand
to trigger save and kendoListViewCancel
to discard changes.
<ng-template kendoListViewEditTemplate let-dataItem="dataItem">
<input [(ngModel)]="dataItem.name" name="name" required/>
<div class="edit-buttons">
<button kendoListViewSaveCommand>Save</button>
<button kendoListViewCancelCommand>Cancel</button>
</div>
</ng-template>
The final conversations-list.component.html
code looks like:
<kendo-listview
[data]="conversations()"
(edit)="editHandler($event)"
(cancel)="cancelHandler($event)"
(save)="saveHandler($event)"
(remove)="removeHandler($event)"
>
<ng-template kendoListViewItemTemplate let-dataItem="dataItem">
<div class="k-d-flex k-justify-content-around k-cursor-pointer">
@if (dataItem.fav) {
<span class="pinned-label">❤️</span>
}
<p (click)="onConversationSelect(dataItem)">{{ dataItem.name }} </p>
<div class="chat-actions">
<button kendoButton (click)="onTogglePin(dataItem)">Pin</button>
<button kendoListViewEditCommand>Edit</button>
<button kendoListViewRemoveCommand>Remove</button>
</div>
</div>
</ng-template>
<ng-template kendoListViewEditTemplate let-dataItem="dataItem">
<input [(ngModel)]="dataItem.name" name="name" class="k-textbox" required/>
<div class="edit-buttons">
<button kendoListViewSaveCommand>Save</button>
<button kendoListViewCancelCommand>Cancel</button>
</div>
</ng-template>
</kendo-listview>
Perfect! We’ve already finished configuring the kendo-listview
and connected it with the events, so now it’s time to bind the input properties and listen for the events from app.component.ts
.
Before starting, import the KENDO_CHAT, ConversationsListComponent and ButtonComponent in the app.component.ts
:
import { Component, signal } from '@angular/core';
import {
ChatConversation,
defaultUser,
firstAIInteraction,
initialConversation,
} from '../entities/chat-entities';
import { ConversationsListComponent } from './components/conversation-list/conversation-list.component';
import {
KENDO_CHAT,
SendMessageEvent,
} from '@progress/kendo-angular-conversational-ui';
import { ButtonComponent } from '@progress/kendo-angular-buttons';
@Component({
selector: 'app-root',
standalone: true,
imports: [ConversationsListComponent, KENDO_CHAT, ButtonComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
First add a button to create new conversation with the kendoButton
directive and bind the createNewConversation
method.
<button kendoButton (click)="createNewConversation()" class="new-conversation-btn">
New Conversation
</button>
Next, we need to pass the inputs to conversation-list
and listen for the events, bind the conversations and conversationSelected using two-way binding [()] and bind the conversationUpdated and conversationDeleted.
<app-conversations-list
(conversationUpdated)="updateConversation($event)"
(conversationDeleted)="deleteConversation($event)"
[(conversationSelected)]="currentConversation"
[(conversations)]="conversations"
></app-conversations-list>
Before continuing, let’s use a wrapper div with class k-d-flex
and also an aside
tag with class k-w-2/6
.
<div class="k-d-flex">
<aside class="k-w-2/6">
<button kendoButton (click)="createNewConversation()" class="new-conversation-btn">
New Conversation
</button>
<app-conversations-list
(conversationUpdated)="updateConversation($event)"
(conversationDeleted)="deleteConversation($event)"
[(conversationSelected)]="currentConversation"
[(conversations)]="conversations"
></app-conversations-list>
</aside>
</div>
Are you curious where the k-d-flex
and k-justify-content-around
classes come from? They are part of the Telerik and Kendo UI CSS utilities. These utilities make it easy to design interfaces without writing your own CSS. They give you ready-to-use styles, which helps save time and effort when building your layouts. I recommend checking out the docs to learn and build nice interfaces with ease!
👉🏽 https://www.telerik.com/design-system/docs/utils/get-started/introduction/.
Finally, we add the kendo-chat
component to add a customizable chat interface out of the box.
The kendo-chat
provide comes with the following key properties to make it easy for us:
[messages]
: Binds the messages array from the current conversation to the chat component.[user]
: Defines the current user interacting with the chat. This is important for differentiating between messages sent by the user and others.(sendMessage)
: Event emitted when user sends message. We bind it to the sendMessage
method in our component.Read more about Kendo Chat.
We use @if
to show the currentConversation
only if it is active, then bind the user with defaultUser
, message with currentConversation().messages
and listen to the (sendMessage)
with updateMessage
method.
@if (currentConversation().active) {
<div class="k-flex-1">
<h2>{{ currentConversation().name }}</h2>
<kendo-chat
[user]="defaultUser"
[messages]="currentConversation().messages"
(sendMessage)="updateMessage($event)"
width="450px"
>
</kendo-chat>
</div>
}
Save the changes, run ng serve, and navigate to http://localhost:4200. There you have it—your chat now supports multiple conversations!
Feel free to play with it by creating new conversations or multiple ones. You can rename, pin or remove conversations. If you send a message, switch to another conversation and come back, your message will still be there, just as you left it.
We learned how to build a chat that supports multiple conversations using Kendo UI components and Angular Signals. When we need to create a chat, managing multiple chats manually can be difficult and take a lot of time, especially when adding features like editing, deleting and marking conversations as favorites.
But when we choose Kendo UI for Angular components, like Conversational UI and ListView, we made our work easier by providing built-in functions that help with tasks like handling messages and displaying chat items. This allows us to develop faster and focus on giving users a better experience in no time.
And don’t forget to give Kendo UI for Angular a try (for free) if you haven’t already!
Dany Paredes is a Google Developer Expert on Angular and Progress Champion. He loves sharing content and writing articles about Angular, TypeScript and testing on his blog and on Twitter (@danywalls).