DevExtreme v25.1 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

Angular Chat - Edit and Delete Messages

The DevExtreme Chat UI component allows users to edit and delete messages as needs dictate.

Use a data source to allow users to edit and delete messages. DevExtreme Chat does not update the data source automatically. Implement a CustomStore with CRUD operations to handle updates. Once you configured these operations, enable editing.

Backend API
<div class="demo-container"> <div class="chat-container"> <dx-chat #chat [height]="600" [dataSource]="dataSource" [reloadOnChange]="false" [user]="currentUser" (onMessageEntered)="onMessageEntered($event)" (onMessageDeleted)="onMessageDeleted($event)" (onMessageUpdated)="onMessageUpdated($event)" > <dxo-chat-editing [allowUpdating]="allowUpdating" [allowDeleting]="allowDeleting" ></dxo-chat-editing> </dx-chat> </div> <div class="options"> <div class="caption">Options</div> <div class="option"> <span>Allow Editing:</span> <dx-select-box [items]="editingStrategies" displayExpr="text" valueExpr="key" [value]="selectedEditingStrategy" [inputAttr]="alloUpdatingLabel" (onValueChanged)="onAllowUpdatingChange($event)" ></dx-select-box> </div> <div class="option"> <span>Allow Deleting:</span> <dx-select-box [items]="editingStrategies" displayExpr="text" valueExpr="key" [value]="selectedDeletingStrategy" [inputAttr]="allowDeletingLabel" (onValueChanged)="onAllowDeletingChange($event)" ></dx-select-box> </div> </div> </div>
import { NgModule, Component, enableProdMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { DxChatModule, DxSelectBoxModule } from 'devextreme-angular'; import { type DxChatTypes } from 'devextreme-angular/ui/chat'; import { DataSource } from 'devextreme-angular/common/data'; import { AppService } from './app.service'; if (!document.location.host.includes('localhost')) { enableProdMode(); } let modulePrefix = ''; // @ts-expect-error if (window && window.config?.packageConfigPaths) { modulePrefix = '/app'; } @Component({ selector: 'demo-app', templateUrl: `app/app.component.html`, styleUrls: [`app/app.component.css`], }) export class AppComponent { currentUser: DxChatTypes.User; allowUpdating = true; allowDeleting = true; editingStrategies = [ { key: 'enabled', text: 'Enabled' }, { key: 'disabled', text: 'Disabled' }, { key: 'custom', text: 'Only the last message (custom)' }, ]; dataSource: DataSource; selectedEditingStrategy = 'enabled'; selectedDeletingStrategy = 'enabled'; allowEditingLabel = this.appService.allowEditingLabel; allowDeletingLabel = this.appService.allowDeletingLabel; editingStrategy: Record<string, any> = { enabled: true, disabled: false, custom: ({ component, message }: any) => { const { items, user } = component.option(); const userId = user.id; const lastNotDeletedMessage = items.findLast( (item: any) => item.author?.id === userId && !item.isDeleted ); return message.id === lastNotDeletedMessage?.id; }, }; constructor(private readonly appService: AppService) { [this.currentUser] = this.appService.getUsers(); this.dataSource = this.appService.dataSource; } onMessageEntered(e: DxChatTypes.MessageEnteredEvent): void { this.appService.onMessageEntered(e); } onMessageDeleted(e: DxChatTypes.MessageDeletedEvent): void { this.appService.onMessageDeleted(e); } onMessageUpdated(e: DxChatTypes.MessageUpdatedEvent): void { this.appService.onMessageUpdated(e); } onAllowUpdatingChange(event: any): void { this.allowUpdating = this.editingStrategy[event.value]; this.selectedEditingStrategy = event.value; } onAllowDeletingChange(event: any): void { this.allowDeleting = this.editingStrategy[event.value]; this.selectedDeletingStrategy = event.value; } } @NgModule({ imports: [ BrowserModule, DxChatModule, DxSelectBoxModule, ], declarations: [AppComponent], bootstrap: [AppComponent], providers: [AppService], }) export class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule);
.demo-container { min-width: 720px; display: flex; gap: 20px; } ::ng-deep .chat-container { display: flex; flex-grow: 1; align-items: center; justify-content: center; } ::ng-deep .options { padding: 20px; display: flex; flex-direction: column; min-width: 280px; background-color: rgba(191, 191, 191, 0.15); gap: 16px; } ::ng-deep .dx-chat { max-width: 480px; } ::ng-deep .caption { font-size: var(--dx-font-size-sm); font-weight: 500; } ::ng-deep .dx-avatar { border: 1px solid var(--dx-color-border); }
import { type DxChatTypes } from 'devextreme-angular/ui/chat'; import { Guid } from 'devextreme-angular/common'; import { CustomStore, DataSource } from 'devextreme-angular/common/data'; export class AppService { date: Date; store: DxChatTypes.Message[] = []; customStore: CustomStore; dataSource: DataSource; currentUser: DxChatTypes.User = { id: 'c94c0e76-fb49-4b9b-8f07-9f93ed93b4f3', name: 'John Doe', }; supportAgent: DxChatTypes.User = { id: 'd16d1a4c-5c67-4e20-b70e-2991c22747c3', name: 'Support Agent', avatarUrl: '../../../../images/petersmith.png', }; messages: DxChatTypes.Message[] = []; allowEditingLabel = { 'aria-label': 'Allow Editing' }; allowDeletingLabel = { 'aria-label': 'Allow Deleting' }; constructor() { this.date = new Date(); this.date.setHours(0, 0, 0, 0); this.messages = [ { id: new Guid().toString(), timestamp: this.getTimestamp(this.date, -9), author: this.supportAgent, text: 'Hello, John!\nHow can I assist you today?', }, { id: new Guid().toString(), timestamp: this.getTimestamp(this.date, -7), author: this.currentUser, text: 'Hi, I\'m having trouble accessing my account.', }, { id: new Guid().toString(), timestamp: this.getTimestamp(this.date, -7), author: this.currentUser, text: 'It says my password is incorrect.', }, { id: new Guid().toString(), timestamp: this.getTimestamp(this.date, -7), author: this.currentUser, isDeleted: true, }, { id: new Guid().toString(), timestamp: this.getTimestamp(this.date, -7), author: this.supportAgent, text: 'I can help you with that. Can you please confirm your UserID for security purposes?', isEdited: true, }, ]; this.initDataSource(); } initDataSource() { this.customStore = new CustomStore({ key: 'id', load: async () => this.messages, insert: async (message) => { this.messages.push(message); return message; }, }); this.dataSource = new DataSource({ store: this.customStore, paginate: false, }); } getUsers(): DxChatTypes.User[] { return [this.currentUser, this.supportAgent]; } getTimestamp(date: Date, offsetMinutes = 0): number { return date.getTime() + offsetMinutes * 60000; } onMessageEntered({ message }: DxChatTypes.MessageEnteredEvent): void { this.dataSource.store().push([{ type: 'insert', data: { id: new Guid().toString(), ...message, }, }]); } onMessageDeleted({ message }: DxChatTypes.MessageDeletedEvent): void { this.dataSource.store().push([{ type: 'update', key: message.id, data: { isDeleted: true }, }]); } onMessageUpdated({ message, text }: DxChatTypes.MessageUpdatedEvent): void { this.dataSource.store().push([{ type: 'update', key: message.id, data: { text, isEdited: true }, }]); } }
// In real applications, you should not transpile code in the browser. // You can see how to create your own application with Angular and DevExtreme here: // https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Create_a_DevExtreme_Application/ const componentNames = [ 'accordion', 'action-sheet', 'autocomplete', 'bar-gauge', 'box', 'bullet', 'button-group', 'button', 'calendar', 'card-view', 'chart', 'chat', 'check-box', 'circular-gauge', 'color-box', 'context-menu', 'data-grid', 'date-box', 'date-range-box', 'defer-rendering', 'diagram', 'draggable', 'drawer', 'drop-down-box', 'drop-down-button', 'file-manager', 'file-uploader', 'filter-builder', 'form', 'funnel', 'gallery', 'gantt', 'html-editor', 'linear-gauge', 'list', 'load-indicator', 'load-panel', 'lookup', 'map', 'menu', 'multi-view', 'nested', 'number-box', 'pagination', 'pie-chart', 'pivot-grid-field-chooser', 'pivot-grid', 'polar-chart', 'popover', 'popup', 'progress-bar', 'radio-group', 'range-selector', 'range-slider', 'recurrence-editor', 'resizable', 'responsive-box', 'sankey', 'scheduler', 'scroll-view', 'select-box', 'slider', 'sortable', 'sparkline', 'speed-dial-action', 'splitter', 'stepper', 'switch', 'tab-panel', 'tabs', 'tag-box', 'text-area', 'text-box', 'tile-view', 'toast', 'toolbar', 'tooltip', 'tree-list', 'tree-map', 'tree-view', 'validation-group', 'validation-summary', 'validator', 'vector-map', ]; window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, }, meta: { 'typescript': { 'exports': 'ts', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, '@angular/platform-browser-dynamic': { 'esModule': true, }, '@angular/platform-browser': { 'esModule': true, }, '@angular/core': { 'esModule': true, }, '@angular/common': { 'esModule': true, }, '@angular/common/http': { 'esModule': true, }, '@angular/animations': { 'esModule': true, }, '@angular/forms': { 'esModule': true, }, 'openai': { 'esModule': true, }, }, paths: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', }, map: { 'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', /* @angular */ '@angular/compiler': 'bundles:@angular/compiler.umd.js', '@angular/platform-browser-dynamic': 'bundles:@angular/platform-browser-dynamic.umd.js', '@angular/core': 'bundles:@angular/core.umd.js', '@angular/core/primitives/signals': 'bundles:@angular/core.primitives.signals.umd.js', '@angular/common': 'bundles:@angular/common.umd.js', '@angular/common/http': 'bundles:@angular/common-http.umd.js', '@angular/platform-browser': 'bundles:@angular/platform-browser.umd.js', '@angular/platform-browser/animations': 'bundles:@angular/platform-browser.umd.js', '@angular/forms': 'bundles:@angular/forms.umd.js', /* devextreme */ 'devextreme': 'npm:devextreme@25.1.3/cjs', 'devextreme-quill': 'npm:devextreme-quill@1.7.3/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.19', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.62', /* devextreme-angular umd maps */ 'devextreme-angular': 'bundles:devextreme-angular/devextreme-angular.umd.js', 'devextreme-angular/common/ai-integration': 'bundles:devextreme-angular/devextreme-angular-common-ai-integration.umd.js', 'devextreme-angular/core': 'bundles:devextreme-angular/devextreme-angular-core.umd.js', 'devextreme-angular/common/charts': 'bundles:devextreme-angular/devextreme-angular-common-charts.umd.js', 'devextreme-angular/common/core/animation': 'bundles:devextreme-angular/devextreme-angular-common-core-animation.umd.js', 'devextreme-angular/common/core/environment': 'bundles:devextreme-angular/devextreme-angular-common-core-environment.umd.js', 'devextreme-angular/common/core/events': 'bundles:devextreme-angular/devextreme-angular-common-core-events.umd.js', 'devextreme-angular/common/core/localization': 'bundles:devextreme-angular/devextreme-angular-common-core-localization.umd.js', 'devextreme-angular/common/core': 'bundles:devextreme-angular/devextreme-angular-common-core.umd.js', 'devextreme-angular/common/data/custom-store': 'bundles:devextreme-angular/devextreme-angular-common-data-custom-store.umd.js', 'devextreme-angular/common/data': 'bundles:devextreme-angular/devextreme-angular-common-data.umd.js', 'devextreme-angular/common/export/excel': 'bundles:devextreme-angular/devextreme-angular-common-export-excel.umd.js', 'devextreme-angular/common/export/pdf': 'bundles:devextreme-angular/devextreme-angular-common-export-pdf.umd.js', 'devextreme-angular/common/export': 'bundles:devextreme-angular/devextreme-angular-common-export.umd.js', 'devextreme-angular/common/grids': 'bundles:devextreme-angular/devextreme-angular-common-grids.umd.js', 'devextreme-angular/common': 'bundles:devextreme-angular/devextreme-angular-common.umd.js', 'devextreme-angular/http': 'bundles:devextreme-angular/devextreme-angular-http.umd.js', ...componentNames.reduce((acc, name) => { acc[`devextreme-angular/ui/${name}`] = `bundles:devextreme-angular/devextreme-angular-ui-${name}.umd.js`; acc[`devextreme-angular/ui/${name}/nested`] = `bundles:devextreme-angular/devextreme-angular-ui-${name}-nested.umd.js`; return acc; }, {}), 'tslib': 'npm:tslib/tslib.js', 'rxjs': 'npm:rxjs@7.5.3/dist/bundles/rxjs.umd.js', 'rxjs/operators': 'npm:rxjs@7.5.3/dist/cjs/operators/index.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign', 'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', '@preact/signals-core': 'npm:@preact/signals-core@1.8.0/dist/signals-core.min.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'app': { main: './app.component.ts', defaultExtension: 'ts', }, 'devextreme': { defaultExtension: 'js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, 'rxjs': { defaultExtension: 'js', }, 'rxjs/operators': { defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', 'npm:rxjs@7.5.3/package.json', 'npm:rxjs@7.5.3/operators/package.json', 'npm:devexpress-diagram@2.2.19/package.json', 'npm:devexpress-gantt@4.1.62/package.json', ], }; System.config(window.config); // System.import('@angular/compiler').catch(console.error.bind(console));
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/25.1.3/css/dx.light.css" /> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/zone.js@0.14.10/bundles/zone.umd.js"></script> <script src="https://cdn.jsdelivr.net/npm/reflect-metadata@0.1.13/Reflect.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script src="config.js"></script> <script> System.import("app").catch(console.error.bind(console)); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <demo-app>Loading...</demo-app> </div> </body> </html>

The editing object includes allowUpdating and allowDeleting properties. These Boolean options are initially set to false. To edit and delete messages, set these Boolean options to true or assign functions with custom logic.

Review this demo and learn how to delete/edit chat messages. First, ensure that "Options" are active in the panel next to the Chat component. Right-click (Control+Click on MacOS) or long-tap a message to open the context menu. Select "Delete" to remove the message; a marker is then displayed in place of the deleted message within the feed. Choose "Edit" to view the original message and update its content. Click "Send" to save changes; this will mark the message as edited.