DevExtreme v25.2 is now available.

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

Your search did not match any results.

Angular Chat - File Attachments

The DevExtreme Chat allows users to attach files. When this feature is activated (fileUploaderOptions.uploadFile is specified), an “Attach” button appears in the message input field, allowing users to add files to their messages.

When users attach files, each file is displayed in the input area with a file-type icon, basic details (name and size), upload status, and an option to remove files before sending.

Backend API
<div class="demo-container"> <div class="chat-container"> <dx-chat [dataSource]="dataSource" [reloadOnChange]="false" [user]="user" height="710" (onMessageEntered)="onMessageEntered($event)" (onAttachmentDownloadClick)="onAttachmentDownloadClick($event)" > <dxo-chat-file-uploader-options [uploadFile]="uploadFile" [onUploaded]="onUploaded" > </dxo-chat-file-uploader-options> </dx-chat> </div> </div>
import { bootstrapApplication } from '@angular/platform-browser'; import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core'; import { DxChatModule } from 'devextreme-angular'; import { DxChatTypes } from 'devextreme-angular/ui/chat'; import { type DxFileUploaderTypes } from 'devextreme-angular/ui/file-uploader'; 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`], imports: [ DxChatModule, ], }) export class AppComponent { dataSource: DataSource; user: DxChatTypes.User; constructor(private readonly appService: AppService) { this.dataSource = this.appService.dataSource; this.user = this.appService.currentUser; } onMessageEntered(e: DxChatTypes.MessageEnteredEvent): void { this.appService.onMessageEntered(e); } onAttachmentDownloadClick(e: DxChatTypes.AttachmentDownloadClickEvent): void { if (e.attachment) { this.appService.onAttachmentDownloadClick(e.attachment); } } onUploaded = (e: DxFileUploaderTypes.UploadedEvent): void => { this.appService.onUploaded(e.file); }; uploadFile = () => {}; } bootstrapApplication(AppComponent, { providers: [ provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), AppService, ], });
.demo-container { min-width: 720px; } .chat-container { display: flex; flex-grow: 1; align-items: center; justify-content: center; } ::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 { Injectable } from '@angular/core'; import { type DxChatTypes } from 'devextreme-angular/ui/chat'; import { Guid } from 'devextreme-angular/common'; import { CustomStore, DataSource } from 'devextreme-angular/common/data'; @Injectable() export class AppService { date: Date; 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[] = []; uploadedFilesMap: Map<string, string>; constructor() { this.date = new Date(); this.date.setHours(0, 0, 0, 0); this.messages = [ { id: new Guid().toString(), timestamp: new Date(this.getTimestamp(this.date, -7)), author: this.currentUser, text: 'Hi! I\'m having trouble accessing my account.\nThe website says my password is incorrect. I\'m sending a few screenshots so you can see where I get the error.', attachments: [ { name: 'Pic1.png', url: '../../../../images/Chat/FileAttachments/Pic1.png', size: 1024 * 10, }, { name: 'Pic2.png', url: '../../../../images/Chat/FileAttachments/Pic2.png', size: 1024 * 10, }, { name: 'Pic3.png', url: '../../../../images/Chat/FileAttachments/Pic3.png', size: 1024 * 10, }, ], }, { id: new Guid().toString(), timestamp: new Date(this.getTimestamp(this.date, -7)), author: this.supportAgent, text: 'Hello! Thanks for including screenshots. To restore access, please follow instructions in the attached file.\nLet me know if you need anything else.', attachments: [ { name: 'Instructions.pdf', url: '../../../../images/Chat/FileAttachments/Instructions.pdf', size: 1024 * 10, }, ], }, ]; this.uploadedFilesMap = new Map(); this.initDataSource(); } getTimestamp(date: Date, offsetMinutes = 0): number { return date.getTime() + offsetMinutes * 60000; } 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, }); } getFileUrl(filename: string): string | undefined { return this.uploadedFilesMap.get(filename); } onUploaded(file: File) { const url = URL.createObjectURL(file); this.uploadedFilesMap.set(file.name, url); } onMessageEntered({ message }: DxChatTypes.MessageEnteredEvent): void { const attachmentsWithUrls = message.attachments?.map((attachment: DxChatTypes.Attachment) => ({ ...attachment, url: this.getFileUrl(attachment.name), })); this.dataSource.store().push([{ type: 'insert', data: { ...message, id: new Guid().toString(), attachments: attachmentsWithUrls, }, }]); } onAttachmentDownloadClick(attachment: DxChatTypes.Attachment) { if (!attachment?.url) { return; } const link = document.createElement('a'); link.setAttribute('href', attachment.url); link.setAttribute('download', attachment.name); document.body.appendChild(link); link.click(); document.body.removeChild(link); } }
// 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', 'speech-to-text', '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/core/primitives/di': 'bundles:@angular/core.primitives.di.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.8/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', 'zone.js': 'npm:zone.js@0.15.1/bundles/zone.umd.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', ], }; window.process = { env: { NODE_ENV: 'production', }, }; System.config(window.config); // eslint-disable-next-line no-console // System.import('@angular/compiler').catch(console.error.bind(console)); // eslint-disable-next-line const useTgzInCSB = ['openai']; let packagesInfo = { "@angular/core": { "version": "21.0.8" }, "core-js": { "version": "2.6.12" }, "typescript": { "version": "5.9.3" }, "zone.js": { "version": "0.15.1" } };
<!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.2.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.15.1/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>

You can customize the file upload process with the following fileUploaderOptions properties:

  • maxFileSize
    Specifies maximum allowed file size.
  • minFileSize
    Specifies minimum allowed file size.
  • multiple
    When set to false, limits uploads to a single file.
  • allowedFileExtensions
    Restricts accepted file types.

For the complete list of configuration options, refer to the following API section: fileUploaderOptions.

Attachment type includes name and size fields. To add custom fields (such as url in this demo), handle the onMessageEntered event and update the message object’s attachments array as needed. You can use this handler to save files to your server.

After a user sends a message, attachments appear in the corresponding message bubble. To allow users to download attachments, implement the onAttachmentDownloadClick event handler. You can define custom download logic within the handler.