Skip to content

Commit 96894c6

Browse files
authored
Chat: support emptyViewTemplate option
1 parent 849ddb6 commit 96894c6

File tree

9 files changed

+191
-10
lines changed

9 files changed

+191
-10
lines changed

packages/devextreme-angular/src/ui/chat/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,19 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges
193193
}
194194

195195

196+
/**
197+
* [descr:dxChatOptions.emptyViewTemplate]
198+
199+
*/
200+
@Input()
201+
get emptyViewTemplate(): any {
202+
return this._getOption('emptyViewTemplate');
203+
}
204+
set emptyViewTemplate(value: any) {
205+
this._setOption('emptyViewTemplate', value);
206+
}
207+
208+
196209
/**
197210
* [descr:dxChatOptions.focusStateEnabled]
198211
@@ -565,6 +578,13 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges
565578
*/
566579
@Output() elementAttrChange: EventEmitter<Record<string, any>>;
567580

581+
/**
582+
583+
* This member supports the internal infrastructure and is not intended to be used directly from your code.
584+
585+
*/
586+
@Output() emptyViewTemplateChange: EventEmitter<any>;
587+
568588
/**
569589
570590
* This member supports the internal infrastructure and is not intended to be used directly from your code.
@@ -769,6 +789,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges
769789
{ emit: 'disabledChange' },
770790
{ emit: 'editingChange' },
771791
{ emit: 'elementAttrChange' },
792+
{ emit: 'emptyViewTemplateChange' },
772793
{ emit: 'focusStateEnabledChange' },
773794
{ emit: 'heightChange' },
774795
{ emit: 'hintChange' },

packages/devextreme-react/src/chat.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ type IChatOptionsNarrowedEvents = {
3131
}
3232

3333
type IChatOptions = React.PropsWithChildren<ReplaceFieldTypes<Properties, IChatOptionsNarrowedEvents> & IHtmlOptions & {
34+
emptyViewRender?: (...params: any) => React.ReactNode;
35+
emptyViewComponent?: React.ComponentType<any>;
3436
messageRender?: (...params: any) => React.ReactNode;
3537
messageComponent?: React.ComponentType<any>;
3638
defaultItems?: Array<Message>;
@@ -72,6 +74,11 @@ const Chat = memo(
7274
}), []);
7375

7476
const templateProps = useMemo(() => ([
77+
{
78+
tmplOption: "emptyViewTemplate",
79+
render: "emptyViewRender",
80+
component: "emptyViewComponent"
81+
},
7582
{
7683
tmplOption: "messageTemplate",
7784
render: "messageRender",

packages/devextreme-vue/src/chat.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type AccessibleOptions = Pick<Properties,
4444
"disabled" |
4545
"editing" |
4646
"elementAttr" |
47+
"emptyViewTemplate" |
4748
"focusStateEnabled" |
4849
"height" |
4950
"hint" |
@@ -89,6 +90,7 @@ const componentConfig = {
8990
disabled: Boolean,
9091
editing: Object as PropType<Record<string, any>>,
9192
elementAttr: Object as PropType<Record<string, any>>,
93+
emptyViewTemplate: {},
9294
focusStateEnabled: Boolean,
9395
height: [Number, String],
9496
hint: String,
@@ -130,6 +132,7 @@ const componentConfig = {
130132
"update:disabled": null,
131133
"update:editing": null,
132134
"update:elementAttr": null,
135+
"update:emptyViewTemplate": null,
133136
"update:focusStateEnabled": null,
134137
"update:height": null,
135138
"update:hint": null,

packages/devextreme/js/__internal/ui/chat/chat.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type {
3131
} from '@ts/ui/chat/messagebox';
3232
import MessageBox from '@ts/ui/chat/messagebox';
3333
import type {
34+
EmptyViewTemplate,
3435
MessageEditingEvent,
3536
MessageTemplate,
3637
Properties as MessageListProperties,
@@ -89,6 +90,7 @@ class Chat extends Widget<Properties> {
8990
dayHeaderFormat: 'shortdate',
9091
messageTemplate: null,
9192
messageTimestampFormat: 'shorttime',
93+
emptyViewTemplate: null,
9294
alerts: [],
9395
showAvatar: true,
9496
showUserName: true,
@@ -201,6 +203,7 @@ class Chat extends Widget<Properties> {
201203
allowDeleting: (message: Message): boolean => this._allowDeleteAction(message),
202204
isEditActionDisabled: (message) => this._messageToEdit === message,
203205
messageTemplate: this._getMessageTemplate(),
206+
emptyViewTemplate: this._getEmptyViewTemplate(),
204207
showDayHeaders,
205208
showAvatar,
206209
showUserName,
@@ -259,17 +262,23 @@ class Chat extends Widget<Properties> {
259262
return allowDeleting ?? false;
260263
}
261264

262-
_getMessageTemplate(): MessageTemplate {
263-
const { messageTemplate } = this.option();
264-
if (messageTemplate) {
265-
return (message, $container): void => {
266-
const template = this._getTemplateByOption('messageTemplate');
265+
_getRenderTemplateFunction(optionName: 'messageTemplate'): MessageTemplate;
266+
_getRenderTemplateFunction(optionName: 'emptyViewTemplate'): EmptyViewTemplate;
267+
_getRenderTemplateFunction(
268+
optionName: 'messageTemplate' | 'emptyViewTemplate',
269+
): MessageTemplate | EmptyViewTemplate {
270+
const { [optionName]: templateOption } = this.option();
271+
272+
if (templateOption) {
273+
return (data, $container): void => {
274+
const template = this._getTemplateByOption(optionName);
275+
const dataFieldName = optionName === 'messageTemplate' ? 'message' : 'data';
267276

268277
template.render({
269278
container: $container,
270279
model: {
271280
component: this,
272-
message,
281+
[dataFieldName]: data,
273282
},
274283
});
275284
};
@@ -278,6 +287,14 @@ class Chat extends Widget<Properties> {
278287
return null;
279288
}
280289

290+
_getMessageTemplate(): MessageTemplate {
291+
return this._getRenderTemplateFunction('messageTemplate');
292+
}
293+
294+
_getEmptyViewTemplate(): EmptyViewTemplate {
295+
return this._getRenderTemplateFunction('emptyViewTemplate');
296+
}
297+
281298
_messageEditingStartHandler(e: MessageEditingEvent): void {
282299
if (this._messageToEdit) {
283300
this._messageEditCanceledAction?.({ message: this._messageToEdit });
@@ -615,6 +632,9 @@ class Chat extends Widget<Properties> {
615632
case 'messageTemplate':
616633
this._messageList.option(name, this._getMessageTemplate());
617634
break;
635+
case 'emptyViewTemplate':
636+
this._messageList.option(name, this._getEmptyViewTemplate());
637+
break;
618638
case 'reloadOnChange':
619639
break;
620640
default:

packages/devextreme/js/__internal/ui/chat/messagelist.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { Message, TextMessage, User } from '@js/ui/chat';
1717
import type { Item as ContextMenuItem } from '@js/ui/context_menu';
1818
import type dxContextMenu from '@js/ui/context_menu';
1919
import type { WidgetOptions } from '@js/ui/widget/ui.widget';
20+
import { getPublicElement } from '@ts/core/m_element';
2021
import type { OptionChanged } from '@ts/core/widget/types';
2122
import Widget from '@ts/core/widget/widget';
2223
import ContextMenu from '@ts/ui/context_menu/context_menu';
@@ -64,6 +65,10 @@ const ESCAPE_KEY = 'escape';
6465
export const MESSAGEGROUP_TIMEOUT = 5 * 1000 * 60;
6566

6667
export type MessageTemplate = ((data: Message, messageBubbleContainer: Element) => void) | null;
68+
export type EmptyViewTemplate = ((
69+
data: { message: string; prompt: string },
70+
emptyViewContainer: Element) => void
71+
) | null;
6772

6873
export type ItemClick = NativeEventInfo<ContextMenu, KeyboardEvent | MouseEvent | PointerEvent> & {
6974
readonly itemData?: ContextMenuItem;
@@ -88,6 +93,7 @@ export interface Properties extends WidgetOptions<MessageList> {
8893
currentUserId: number | string | undefined;
8994
showDayHeaders: boolean;
9095
messageTemplate?: MessageTemplate;
96+
emptyViewTemplate?: EmptyViewTemplate;
9197
dayHeaderFormat?: Format;
9298
messageTimestampFormat?: Format;
9399
typingUsers: User[];
@@ -131,6 +137,7 @@ class MessageList extends Widget<Properties> {
131137
showAvatar: true,
132138
showUserName: true,
133139
showMessageTimestamp: true,
140+
emptyViewTemplate: null,
134141
messageTemplate: null,
135142
};
136143
}
@@ -198,23 +205,34 @@ class MessageList extends Widget<Properties> {
198205
}
199206

200207
_renderEmptyViewContent(): void {
208+
const messageText = messageLocalization.format('dxChat-emptyListMessage');
209+
const promptText = messageLocalization.format('dxChat-emptyListPrompt');
210+
const { emptyViewTemplate } = this.option();
211+
201212
const $emptyView = $('<div>')
202213
.addClass(CHAT_MESSAGELIST_EMPTY_VIEW_CLASS)
203214
.attr('id', `dx-${new Guid()}`);
204215

216+
if (emptyViewTemplate) {
217+
const data = {
218+
message: messageText,
219+
prompt: promptText,
220+
};
221+
emptyViewTemplate(data, getPublicElement($emptyView));
222+
$emptyView.appendTo(this._$content);
223+
224+
return;
225+
}
226+
205227
$('<div>')
206228
.appendTo($emptyView)
207229
.addClass(CHAT_MESSAGELIST_EMPTY_IMAGE_CLASS);
208230

209-
const messageText = messageLocalization.format('dxChat-emptyListMessage');
210-
211231
$('<div>')
212232
.appendTo($emptyView)
213233
.addClass(CHAT_MESSAGELIST_EMPTY_MESSAGE_CLASS)
214234
.text(messageText);
215235

216-
const promptText = messageLocalization.format('dxChat-emptyListPrompt');
217-
218236
$('<div>')
219237
.appendTo($emptyView)
220238
.addClass(CHAT_MESSAGELIST_EMPTY_PROMPT_CLASS)
@@ -809,6 +827,7 @@ class MessageList extends Widget<Properties> {
809827
case 'showUserName':
810828
case 'showMessageTimestamp':
811829
case 'messageTemplate':
830+
case 'emptyViewTemplate':
812831
case 'dayHeaderFormat':
813832
case 'messageTimestampFormat':
814833
this._invalidate();

packages/devextreme/js/ui/chat.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,15 @@ export type MessageTemplateData = {
284284
readonly message?: Message;
285285
};
286286

287+
/** @public */
288+
export type EmptyViewTemplateData = {
289+
readonly component: dxChat;
290+
readonly data: {
291+
readonly message: string;
292+
readonly prompt: string;
293+
};
294+
};
295+
287296
/**
288297
* @deprecated use Properties instead
289298
* @namespace DevExpress.ui
@@ -339,6 +348,13 @@ export interface dxChatOptions extends WidgetOptions<dxChat> {
339348
*/
340349
allowDeleting?: boolean | ((options: { component?: dxChat; message?: Message }) => boolean);
341350
};
351+
/**
352+
* @docid
353+
* @default null
354+
* @type_function_return string|Element|jQuery
355+
* @public
356+
*/
357+
emptyViewTemplate?: template | null | ((data: EmptyViewTemplateData, itemElement: DxElement) => string | UserDefinedElement);
342358
/**
343359
* @docid
344360
* @type string | Array<Message> | Store | DataSource | DataSourceOptions | null

packages/devextreme/js/ui/chat_types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export {
1717
ImageMessage,
1818
Message,
1919
MessageTemplateData,
20+
EmptyViewTemplateData,
2021
dxChatOptions,
2122
Properties,
2223
} from './chat';

packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import fx from 'common/core/animation/fx';
2828
import { isRenderer } from 'core/utils/type';
2929

3030
import config from 'core/config';
31+
import localization from 'localization';
3132
import ArrayStore from 'common/data/array_store';
3233
import {
3334
CHAT_EDITING_PREVIEW_CLASS,
@@ -122,6 +123,7 @@ const moduleConfig = {
122123
this.getContextMenuItems = () => $(this.getContextMenu().itemsContainer()).find(`.${DX_MENU_ITEM_CLASS}`);
123124
this.getEditingPreview = () => this.$element.find(`.${CHAT_EDITING_PREVIEW_CLASS}`);
124125
this.getCancelEditingButton = () => this.$element.find(`.${CHAT_EDITING_PREVIEW_CANCEL_BUTTON_CLASS}`);
126+
this.getMessageListEmptyView = () => this.$element.find(`.${CHAT_MESSAGELIST_EMPTY_VIEW_CLASS}`);
125127

126128
init();
127129
},
@@ -758,6 +760,81 @@ QUnit.module('Chat', () => {
758760
});
759761
});
760762

763+
QUnit.module('emptyViewTemplate', () => {
764+
QUnit.test('emptyViewTemplate should set empty view content on init', function(assert) {
765+
this.reinit({
766+
emptyViewTemplate: () => $('<h1>').text('This is empty'),
767+
});
768+
769+
const $emptyView = this.getMessageListEmptyView();
770+
771+
assert.strictEqual($emptyView.text(), 'This is empty');
772+
});
773+
774+
QUnit.test('emptyViewTemplate should set empty view content at runtime', function(assert) {
775+
this.reinit({ });
776+
this.instance.option('emptyViewTemplate', () => $('<h1>').text('This is empty'));
777+
778+
const $emptyView = this.getMessageListEmptyView();
779+
780+
assert.strictEqual($emptyView.text(), 'This is empty');
781+
});
782+
783+
QUnit.test('emptyViewTemplate specified as a string text should set empty view content', function(assert) {
784+
this.reinit({ emptyViewTemplate: 'empty' });
785+
786+
const $emptyView = this.getMessageListEmptyView();
787+
788+
assert.strictEqual($emptyView.text(), 'empty');
789+
});
790+
791+
QUnit.test('emptyViewTemplate specified as a string with a html element should set empty view content', function(assert) {
792+
this.reinit({ emptyViewTemplate: '<p>p text</p>' });
793+
794+
const $emptyViewChild = this.getMessageListEmptyView().children();
795+
796+
assert.strictEqual($emptyViewChild.text(), 'p text', 'template text is correct');
797+
assert.strictEqual($emptyViewChild.prop('tagName'), 'P', 'templte tag element is correct');
798+
});
799+
800+
QUnit.test('emptyViewTemplate function argument should include Chat instance', function(assert) {
801+
assert.expect(1);
802+
803+
const emptyViewTemplate = (data) => {
804+
assert.strictEqual(data.component instanceof Chat, true, 'chat instance is passed');
805+
};
806+
807+
this.reinit({ emptyViewTemplate });
808+
});
809+
810+
QUnit.test('emptyViewTemplate function argument should include data with localized message and prompt', function(assert) {
811+
assert.expect(2);
812+
813+
const defaultLocale = localization.locale();
814+
const localizedEmptyListMessage = 'Lista wiadomości jest pusta';
815+
const localizedEmptyListPrompt = 'Napisz swoją pierwszą wiadomość';
816+
817+
const emptyViewTemplate = ({ data }) => {
818+
assert.strictEqual(data.message, localizedEmptyListMessage, 'localized message is passed');
819+
assert.strictEqual(data.prompt, localizedEmptyListPrompt, 'localized prompt is passed');
820+
};
821+
822+
try {
823+
localization.loadMessages({
824+
'pl': {
825+
'dxChat-emptyListMessage': localizedEmptyListMessage,
826+
'dxChat-emptyListPrompt': localizedEmptyListPrompt,
827+
}
828+
});
829+
localization.locale('pl');
830+
831+
this.reinit({ emptyViewTemplate });
832+
} finally {
833+
localization.locale(defaultLocale);
834+
}
835+
});
836+
});
837+
761838
QUnit.test('dayHeaderFormat option value should be passed to messageList on init', function(assert) {
762839
const dayHeaderFormat = 'dd of MMMM, yyyy';
763840

0 commit comments

Comments
 (0)