DevExtreme v25.1 is now available.

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

Your search did not match any results.

Vue 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
<template> <div class="chat-container"> <DxChat :height="600" :data-source="dataSource" :user="currentUser" :reload-on-change="false" @messageEntered="onMessageEntered" @messageDeleted="onMessageDeleted" @messageUpdated="onMessageUpdated" > <DxEditing :allow-deleting="allowDeleting" :allow-updating="allowUpdating" /> </DxChat> </div> <div class="options"> <div class="caption">Options</div> <div class="option"> <span>Allow Editing:</span> <DxSelectBox :items="editingOptions" value-expr="key" display-expr="text" :input-attr="allowEditingLabel" :value="selectedEditingStrategy" @valueChanged="onAllowEditingChange" /> </div> <div class="option"> <span>Allow Deleting:</span> <DxSelectBox :items="editingOptions" value-expr="key" display-expr="text" :input-attr="allowDeletingLabel" :value="selectedDeletingStrategy" @valueChanged="onAllowDeletingChange" /> </div> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue'; import { DxChat, DxEditing } from 'devextreme-vue/chat'; import { DxSelectBox } from 'devextreme-vue/select-box'; import { Guid } from 'devextreme-vue/common'; import { CustomStore, DataSource } from 'devextreme-vue/common/data'; import { messages as initialMessages, currentUser, editingOptions, allowEditingLabel, allowDeletingLabel, } from './data.ts'; const store = [...initialMessages]; const customStore = new CustomStore({ key: 'id', load: async() => store, insert: async(message) => { store.push(message); return message; }, }); const dataSource = computed(() => new DataSource({ store: customStore, paginate: false, })); const onMessageEntered = ({ message }) => { const newMessage = { id: new Guid().toString(), ...message, }; dataSource.value.store().push([ { type: 'insert', key: newMessage.id, data: newMessage, }, ]); }; const onMessageDeleted = ({ message }) => { dataSource.value.store().push([ { type: 'update', key: message.id, data: { isDeleted: true }, }, ]); }; const onMessageUpdated = ({ message, text }) => { dataSource.value.store().push([ { type: 'update', key: message.id, data: { text, isEdited: true }, }, ]); }; const editingStrategy = { enabled: true, disabled: false, custom: ({ component, message }) => { const { items, user } = component.option(); const userId = user.id; const lastNotDeletedMessage = items.findLast( (item) => item.author?.id === userId && !item.isDeleted ); return message.id === lastNotDeletedMessage?.id; }, }; const selectedEditingStrategy = ref('enabled'); const selectedDeletingStrategy = ref('enabled'); const allowUpdating = ref(editingStrategy[selectedEditingStrategy.value]); const allowDeleting = ref(editingStrategy[selectedDeletingStrategy.value]); const onAllowEditingChange = (event) => { selectedEditingStrategy.value = event.value; allowUpdating.value = editingStrategy[event.value]; }; const onAllowDeletingChange = (event) => { selectedDeletingStrategy.value = event.value; allowDeleting.value = editingStrategy[event.value]; }; </script> <style scoped> #app { min-width: 720px; display: flex; gap: 20px; } .chat-container { display: flex; flex-grow: 1; align-items: center; justify-content: center; } .options { padding: 20px; display: flex; flex-direction: column; min-width: 280px; background-color: rgba(191, 191, 191, 0.15); gap: 16px; } .dx-chat { max-width: 480px; } .caption { font-size: var(--dx-font-size-sm); font-weight: 500; } .option-separator { border-bottom: 1px solid var(--dx-color-border); } .dx-avatar { border: 1px solid var(--dx-color-border); } </style>
window.exports = window.exports || {}; window.config = { transpiler: 'plugin-babel', meta: { '*.vue': { loader: 'vue-loader', }, '*.ts': { loader: 'demo-ts-loader', }, '*.svg': { loader: 'svg-loader', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, 'openai': { 'esModule': true, }, }, paths: { 'project:': '../../../../', 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', }, map: { 'vue': 'npm:vue@3.4.27/dist/vue.esm-browser.js', '@vue/shared': 'npm:@vue/shared@3.4.27/dist/shared.cjs.prod.js', 'vue-loader': 'npm:dx-systemjs-vue-browser@1.1.2/index.js', 'demo-ts-loader': 'project:utils/demo-ts-loader.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'svg-loader': 'project:utils/svg-loader.js', 'mitt': 'npm:mitt/dist/mitt.umd.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', 'devextreme': 'npm:devextreme@25.1.3/cjs', 'devextreme-vue': 'npm:devextreme-vue@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/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.62/dist/dx-gantt.js', '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', 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'devextreme-vue': { main: 'index.js', }, 'devextreme-vue/common': { main: 'index.js', }, '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', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', ], babelOptions: { sourceMaps: false, stage0: true, }, }; System.config(window.config);
import { Guid } from 'devextreme-vue/common'; function getTimestamp(date, offsetMinutes = 0): number { return date.getTime() + offsetMinutes * 60000; } const date = new Date(); date.setHours(0, 0, 0, 0); export const currentUser = { id: 'c94c0e76-fb49-4b9b-8f07-9f93ed93b4f3', name: 'John Doe', }; export const supportAgent = { id: 'd16d1a4c-5c67-4e20-b70e-2991c22747c3', name: 'Support Agent', avatarUrl: '../../../../images/petersmith.png', }; export const messages = [ { id: new Guid().toString(), timestamp: getTimestamp(date, -9), author: supportAgent, text: 'Hello, John!\nHow can I assist you today?', }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, text: 'Hi, I\'m having trouble accessing my account.', }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, text: 'It says my password is incorrect.', }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: currentUser, isDeleted: true, }, { id: new Guid().toString(), timestamp: getTimestamp(date, -7), author: supportAgent, text: 'I can help you with that. Can you please confirm your UserID for security purposes?', isEdited: true, }, ]; export const allowEditingLabel = { 'aria-label': 'Allow Editing' }; export const allowDeletingLabel = { 'aria-label': 'Allow Deleting' }; export const editingOptions = [ { text: 'Enabled', key: 'enabled' }, { text: 'Disabled', key: 'disabled' }, { text: 'Only the last message (custom)', key: 'custom' }, ];
import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app');
<!DOCTYPE html> <html 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 type="module"> import * as vueCompilerSFC from "https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@3.4.27/dist/compiler-sfc.esm-browser.js"; window.vueCompilerSFC = vueCompilerSFC; </script> <script src="https://cdn.jsdelivr.net/npm/typescript@5.4.5/lib/typescript.js"></script> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.ts"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </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.