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@link:../../packages/devextreme/artifacts/npm/devextreme/cjs',
'devextreme-quill': 'npm:devextreme-quill@1.7.6/dist/dx-quill.min.js',
'devexpress-diagram': 'npm:devexpress-diagram@2.2.24',
'devexpress-gantt': 'npm:devexpress-gantt@4.1.64',
/* 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',
'devextreme-angular/core/tokens': 'bundles:devextreme-angular/devextreme-angular-core-tokens.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.24/package.json',
'npm:devexpress-gantt@4.1.64/package.json',
],
};
System.config(window.config);
// System.import('@angular/compiler').catch(console.error.bind(console));
// eslint-disable-next-line
const useTgzInCSB = ['openai'];
let packagesInfo = {
"@angular/core": {
"version": "17.3.12"
},
"core-js": {
"version": "2.6.12"
},
"typescript": {
"version": "5.4.5"
},
"zone.js": {
"version": "0.14.10"
}
};
<!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.7/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.