Telerik blogs

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!

Setting Up the Project

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!

Create Conversation Entities and Mock Data

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,
};

Managing Conversation States with Angular Signals

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: the user for our chats, to use in the kendo-chat component.
  • currentConversation: represents the active conversation.
  • Conversations: stores all our conversations with our messages.
  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 Conversation List

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.

Signal Model

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.

Handle the Events

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.

  • editHandler: When the user clicks the “Edit” button on a conversation
  • saveHandler: When the user clicks “Save,” we emit the updated conversation to the app.component and close the editor.
  • cancelHandler: If the user cancels, we discard the changes and close the editor.
  • removeHandler: When user clicks delete, it emits the ID of the deleted conversation to the 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.

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

List Conversations and Kendo Chat

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>

WAIT A SECOND!

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.

Conclusion

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!

Try Now


About the Author

Dany Paredes

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

Related Posts

Comments

Comments are disabled in preview mode.