diff --git a/package-lock.json b/package-lock.json index d1b5e36f542..4f2e7b11984 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,7 +71,7 @@ "ig-typedoc-theme": "^6.0.0", "igniteui-dockmanager": "^1.17.0", "igniteui-sassdoc-theme": "^2.0.2", - "igniteui-webcomponents": "6.2.1", + "igniteui-webcomponents": "^6.3.1", "jasmine": "^5.6.0", "jasmine-core": "^5.6.0", "karma": "^6.4.4", @@ -614,15 +614,15 @@ } }, "node_modules/@angular/build/node_modules/@types/node": { - "version": "24.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz", - "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==", + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.0.tgz", + "integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==", "dev": true, "license": "MIT", "optional": true, "peer": true, "dependencies": { - "undici-types": "~7.12.0" + "undici-types": "~7.13.0" } }, "node_modules/@angular/build/node_modules/sass": { @@ -647,9 +647,9 @@ } }, "node_modules/@angular/build/node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", "dev": true, "license": "MIT", "optional": true, @@ -5775,22 +5775,22 @@ } }, "node_modules/@shikijs/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.6.0.tgz", - "integrity": "sha512-9By7Xb3olEX0o6UeJyPLI1PE1scC4d3wcVepvtv2xbuN9/IThYN4Wcwh24rcFeASzPam11MCq8yQpwwzCgSBRw==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.13.0.tgz", + "integrity": "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.6.0", + "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "node_modules/@shikijs/core/node_modules/@shikijs/types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz", - "integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz", + "integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==", "dev": true, "license": "MIT", "dependencies": { @@ -5799,21 +5799,21 @@ } }, "node_modules/@shikijs/engine-javascript": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.6.0.tgz", - "integrity": "sha512-7YnLhZG/TU05IHMG14QaLvTW/9WiK8SEYafceccHUSXs2Qr5vJibUwsDfXDLmRi0zHdzsxrGKpSX6hnqe0k8nA==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.13.0.tgz", + "integrity": "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.6.0", + "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "node_modules/@shikijs/engine-javascript/node_modules/@shikijs/types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz", - "integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz", + "integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==", "dev": true, "license": "MIT", "dependencies": { @@ -5833,19 +5833,19 @@ } }, "node_modules/@shikijs/langs": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.6.0.tgz", - "integrity": "sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.13.0.tgz", + "integrity": "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.6.0" + "@shikijs/types": "3.13.0" } }, "node_modules/@shikijs/langs/node_modules/@shikijs/types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz", - "integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz", + "integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==", "dev": true, "license": "MIT", "dependencies": { @@ -5854,19 +5854,19 @@ } }, "node_modules/@shikijs/themes": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.6.0.tgz", - "integrity": "sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.13.0.tgz", + "integrity": "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.6.0" + "@shikijs/types": "3.13.0" } }, "node_modules/@shikijs/themes/node_modules/@shikijs/types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz", - "integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz", + "integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==", "dev": true, "license": "MIT", "dependencies": { @@ -13648,9 +13648,9 @@ } }, "node_modules/igniteui-webcomponents": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/igniteui-webcomponents/-/igniteui-webcomponents-6.2.1.tgz", - "integrity": "sha512-nsErVEF/2nuU76w8pkDzdu+0Xwv25OYWVDdXP5dFoQwvLMusNFju273e8c+DV9LoPtD0nWx6+RzyNaS+ylWXjw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/igniteui-webcomponents/-/igniteui-webcomponents-6.3.1.tgz", + "integrity": "sha512-t0D5xpBmtLLaPGAdfsX+u/nyTBUPm4W13sx777YiiTBH5MpYqEkrLirYi1GTJNQ6v7prepUuGOCj7yuSx/MJ3g==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -13661,6 +13661,26 @@ }, "engines": { "node": ">=20" + }, + "peerDependencies": { + "dompurify": "^3.2.0", + "marked": "^16.3.0", + "marked-shiki": "^1.2.0", + "shiki": "^3.12.0" + }, + "peerDependenciesMeta": { + "dompurify": { + "optional": true + }, + "marked": { + "optional": true + }, + "marked-shiki": { + "optional": true + }, + "shiki": { + "optional": true + } } }, "node_modules/ignore": { @@ -16292,19 +16312,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/marked": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.3.tgz", - "integrity": "sha512-Fqa7eq+UaxfMriqzYLayfqAE40WN03jf+zHjT18/uXNuzjq3TY0XTbrAoPeqSJrAmPz11VuUA+kBPYOhHt9oOQ==", - "dev": true, - "license": "MIT", - "bin": { - "marked": "bin/marked" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -21236,6 +21243,19 @@ "marked": "^0.6.2" } }, + "node_modules/sassdoc-extras/node_modules/marked": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.3.tgz", + "integrity": "sha512-Fqa7eq+UaxfMriqzYLayfqAE40WN03jf+zHjT18/uXNuzjq3TY0XTbrAoPeqSJrAmPz11VuUA+kBPYOhHt9oOQ==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sassdoc-plugin-localization": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sassdoc-plugin-localization/-/sassdoc-plugin-localization-2.0.0.tgz", @@ -22254,37 +22274,37 @@ } }, "node_modules/shiki": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.6.0.tgz", - "integrity": "sha512-tKn/Y0MGBTffQoklaATXmTqDU02zx8NYBGQ+F6gy87/YjKbizcLd+Cybh/0ZtOBX9r1NEnAy/GTRDKtOsc1L9w==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.13.0.tgz", + "integrity": "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "3.6.0", - "@shikijs/engine-javascript": "3.6.0", - "@shikijs/engine-oniguruma": "3.6.0", - "@shikijs/langs": "3.6.0", - "@shikijs/themes": "3.6.0", - "@shikijs/types": "3.6.0", + "@shikijs/core": "3.13.0", + "@shikijs/engine-javascript": "3.13.0", + "@shikijs/engine-oniguruma": "3.13.0", + "@shikijs/langs": "3.13.0", + "@shikijs/themes": "3.13.0", + "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "node_modules/shiki/node_modules/@shikijs/engine-oniguruma": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.6.0.tgz", - "integrity": "sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.13.0.tgz", + "integrity": "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.6.0", + "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/shiki/node_modules/@shikijs/types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz", - "integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.13.0.tgz", + "integrity": "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index db833f8676f..28a78b306a4 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "ig-typedoc-theme": "^6.0.0", "igniteui-dockmanager": "^1.17.0", "igniteui-sassdoc-theme": "^2.0.2", - "igniteui-webcomponents": "6.2.1", + "igniteui-webcomponents": "^6.3.1", "jasmine": "^5.6.0", "jasmine-core": "^5.6.0", "karma": "^6.4.4", diff --git a/projects/igniteui-angular/src/lib/chat/chat.component.html b/projects/igniteui-angular/src/lib/chat/chat.component.html new file mode 100644 index 00000000000..163299e8727 --- /dev/null +++ b/projects/igniteui-angular/src/lib/chat/chat.component.html @@ -0,0 +1,13 @@ + diff --git a/projects/igniteui-angular/src/lib/chat/chat.component.ts b/projects/igniteui-angular/src/lib/chat/chat.component.ts new file mode 100644 index 00000000000..7536994c7b0 --- /dev/null +++ b/projects/igniteui-angular/src/lib/chat/chat.component.ts @@ -0,0 +1,241 @@ +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA, + Directive, + effect, + inject, + input, + OnInit, + output, + signal, + TemplateRef, + ViewContainerRef, + untracked, + OnDestroy, + ViewRef, +} from '@angular/core'; +import { + IgcChatComponent, + type IgcChatMessageAttachment, + type IgcChatMessage, + type IgcChatOptions, + type ChatRenderContext, + type ChatRenderers, + type ChatAttachmentRenderContext, + type ChatInputRenderContext, + type ChatMessageRenderContext, +} from 'igniteui-webcomponents'; + +type ChatContextUnion = + | ChatAttachmentRenderContext + | ChatMessageRenderContext + | ChatInputRenderContext + | ChatRenderContext; + +type ChatContextType = + T extends ChatAttachmentRenderContext + ? IgcChatMessageAttachment + : T extends ChatMessageRenderContext + ? IgcChatMessage + : T extends ChatInputRenderContext + ? { value: T['value']; attachments: T['attachments'] } + : T extends ChatRenderContext + ? { instance: T['instance'] } + : never; + +type ExtractChatContext = T extends (ctx: infer R) => any ? R : never; + +type ChatTemplatesContextMap = { + [K in keyof ChatRenderers]: { + $implicit: ChatContextType< + ExtractChatContext> & ChatContextUnion + >; + }; +}; + +export type NgChatTemplates = { + [K in keyof ChatRenderers]?: TemplateRef; +}; + +export type NgChatOptions = Omit; + + +@Component({ + selector: 'igx-chat', + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + templateUrl: './chat.component.html', +}) +export class IgxChatComponent implements OnInit, OnDestroy { + //#region Internal state + + private readonly _view = inject(ViewContainerRef); + private readonly _templateViewRefs = new Map, Set>(); + private _oldTemplates: NgChatTemplates = {}; + + protected readonly _mergedOptions = signal({}); + protected readonly _transformedTemplates = signal({}); + + //#endregion + + //#region Inputs + + public readonly messages = input([]); + public readonly draftMessage = input< + { text: string; attachments?: IgcChatMessageAttachment[] } | undefined + >({ text: '' }); + public readonly options = input({}); + public readonly templates = input({}); + + //#endregion + + //#region Outputs + + public readonly messageCreated = output(); + public readonly attachmentClick = output(); + public readonly attachmentDrag = output(); + public readonly attachmentDrop = output(); + public readonly typingChange = output(); + public readonly inputFocus = output(); + public readonly inputBlur = output(); + public readonly inputChange = output(); + + //#endregion + + /** @internal */ + public ngOnInit(): void { + IgcChatComponent.register(); + } + + /** @internal */ + public ngOnDestroy(): void { + for (const viewSet of this._templateViewRefs.values()) { + viewSet.forEach(viewRef => viewRef.destroy()); + } + this._templateViewRefs.clear(); + } + + constructor() { + // Templates changed - update transformed templates and viewRefs and merge with options + effect(() => { + const templates = this.templates(); + this._setTemplates(templates ?? {}); + this._mergeOptions(untracked(() => this.options())); + }); + + // Options changed - merge with current template state + effect(() => { + const options = this.options(); + this._mergeOptions(options ?? {}); + }); + } + + private _mergeOptions(options: NgChatOptions): void { + const transformedTemplates = this._transformedTemplates(); + const merged: IgcChatOptions = { + ...options, + renderers: transformedTemplates + }; + this._mergedOptions.set(merged); + } + + private _setTemplates(newTemplates: NgChatTemplates): void { + const templateCopies: ChatRenderers = {}; + const newTemplateKeys = Object.keys(newTemplates) as Array; + + const oldTemplates = this._oldTemplates; + const oldTemplateKeys = Object.keys(oldTemplates) as Array; + + for (const key of oldTemplateKeys) { + const oldRef = oldTemplates[key]; + const newRef = newTemplates[key]; + + if (oldRef && oldRef !== newRef) { + const obsolete = this._templateViewRefs.get(oldRef); + if (obsolete) { + obsolete.forEach(viewRef => viewRef.destroy()); + this._templateViewRefs.delete(oldRef); + } + } + } + + if (newTemplateKeys.length > 0) { + this._oldTemplates = {}; + for (const key of newTemplateKeys) { + const ref = newTemplates[key]; + if (ref) { + this._oldTemplates[key] = ref as any; + templateCopies[key] = this._createTemplateRenderer(ref); + } + } + } + + this._transformedTemplates.set(templateCopies); + } + + private _createTemplateRenderer(ref: NonNullable) { + type ChatContext = ExtractChatContext>; + + if (!this._templateViewRefs.has(ref)) { + this._templateViewRefs.set(ref, new Set()); + } + + const viewSet = this._templateViewRefs.get(ref)!; + + return (ctx: ChatContext) => { + const context = ctx as ChatContextUnion; + let angularContext: any; + + if ('message' in context && 'attachment' in context) { + angularContext = { $implicit: context.attachment }; + } else if ('message' in context) { + angularContext = { $implicit: context.message }; + } else if ('value' in context) { + angularContext = { + $implicit: { value: context.value, attachments: context.attachments }, + }; + } else { + angularContext = { $implicit: { instance: context.instance } }; + } + + const viewRef = this._view.createEmbeddedView(ref, angularContext); + viewSet.add(viewRef); + + return viewRef.rootNodes; + } + } +} + +export interface ChatTemplateContext { + $implicit: T; +} + +interface ChatInputContext { + $implicit: string; + attachments: IgcChatMessageAttachment[]; +} + +@Directive({ selector: '[igxChatMessageContext]' }) +export class IgxChatMessageContextDirective { + + public static ngTemplateContextGuard(_: IgxChatMessageContextDirective, ctx: unknown): ctx is ChatTemplateContext { + return true; + } +}; + +@Directive({ selector: '[igxChatAttachmentContext]' }) +export class IgxChatAttachmentContextDirective { + + public static ngTemplateContextGuard(_: IgxChatAttachmentContextDirective, ctx: unknown): ctx is ChatTemplateContext { + return true; + } +} + +@Directive({ selector: '[igxChatInputContext]' }) +export class IgxChatInputContextDirective { + + public static ngTemplateContextGuard(_: IgxChatInputContextDirective, ctx: unknown): ctx is ChatInputContext { + return true; + } +} diff --git a/projects/igniteui-angular/src/lib/chat/chat.spec.ts b/projects/igniteui-angular/src/lib/chat/chat.spec.ts new file mode 100644 index 00000000000..157a0f1cdf1 --- /dev/null +++ b/projects/igniteui-angular/src/lib/chat/chat.spec.ts @@ -0,0 +1,177 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { IgxChatComponent, IgxChatMessageContextDirective, NgChatTemplates } from './chat.component' +import { Component, signal, TemplateRef, viewChild } from '@angular/core'; +import type { IgcChatComponent, IgcChatMessage, IgcTextareaComponent } from 'igniteui-webcomponents'; + +describe('Chat wrapper', () => { + + let chatComponent: IgxChatComponent; + let chatElement: IgcChatComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [IgxChatComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(IgxChatComponent); + chatComponent = fixture.componentInstance; + chatElement = getChatElement(fixture); + fixture.detectChanges(); + }) + + it('is created', () => { + expect(chatComponent).toBeDefined(); + }); + + it('has correct initial empty state', () => { + const draft = chatComponent.draftMessage(); + + expect(chatComponent.messages().length).toEqual(0); + expect(draft.text).toEqual(''); + expect(draft.attachments).toBeUndefined(); + }); + + it('correct bindings for messages', async () => { + fixture.componentRef.setInput('messages', [{ id: '1', sender: 'user', text: 'Hello' }]); + + fixture.detectChanges(); + await fixture.whenStable(); + + + const messageElement = getChatMessages(chatElement)[0]; + expect(messageElement).toBeDefined(); + expect(getChatMessageDOM(messageElement).textContent.trim()).toEqual(chatComponent.messages()[0].text); + }); + + it('correct bindings for draft message', async () => { + fixture.componentRef.setInput('draftMessage', { text: 'Hello world' }); + + fixture.detectChanges(); + await fixture.whenStable(); + + const textarea = getChatInput(chatElement); + expect(textarea.value).toEqual(chatComponent.draftMessage().text); + }); +}); + +describe('Chat templates', () => { + let fixture: ComponentFixture; + let chatElement: IgcChatComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [IgxChatComponent, IgxChatMessageContextDirective, ChatTemplatesBed] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChatTemplatesBed); + fixture.detectChanges(); + chatElement = getChatElement(fixture); + }); + + it('has correct initially bound template', async () => { + await fixture.whenStable(); + + // NOTE: This is invoked since in the test bed there is no app ref so fresh embedded view + // has no change detection ran on it. In an application scenario this is not the case. + // This is so we don't explicitly invoke `viewRef.detectChanges()` inside the returned closure + // from the wrapper's `_createTemplateRenderer` call. + fixture.detectChanges(); + expect(getChatMessageDOM(getChatMessages(chatElement)[0]).textContent.trim()) + .toEqual(`Your message: ${fixture.componentInstance.messages()[0].text}`); + }); +}); + +describe('Chat dynamic templates binding', () => { + let fixture: ComponentFixture; + let chatElement: IgcChatComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [IgxChatComponent, IgxChatMessageContextDirective, ChatDynamicTemplatesBed] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChatDynamicTemplatesBed); + fixture.detectChanges(); + chatElement = getChatElement(fixture); + }); + + it('supports late binding', async () => { + fixture.componentInstance.bindTemplates(); + fixture.detectChanges(); + + await fixture.whenStable(); + fixture.detectChanges(); + + expect(getChatMessageDOM(getChatMessages(chatElement)[0]).textContent.trim()) + .toEqual(`Your message: ${fixture.componentInstance.messages()[0].text}`); + }); + +}); + + +@Component({ + template: ` + + +

Your message: {{ message.text }}

+
+ `, + imports: [IgxChatComponent, IgxChatMessageContextDirective] +}) +class ChatTemplatesBed { + public messages = signal([{ + id: '1', + sender: 'user', + text: 'Hello world' + }]); + public messageTemplate = viewChild.required>('message'); +} + +@Component({ + template: ` + + +

Your message: {{ message.text }}

+
+ `, + imports: [IgxChatComponent, IgxChatMessageContextDirective] +}) +class ChatDynamicTemplatesBed { + public templates = signal(null); + public messages = signal([{ + id: '1', + sender: 'user', + text: 'Hello world' + }]); + public messageTemplate = viewChild.required>('message'); + + public bindTemplates(): void { + this.templates.set({ + messageContent: this.messageTemplate() + }); + } +} + +function getChatElement(fixture: ComponentFixture): IgcChatComponent { + const nativeElement = fixture.nativeElement as HTMLElement; + return nativeElement.querySelector('igc-chat'); +} + +function getChatInput(chat: IgcChatComponent): IgcTextareaComponent { + return chat.renderRoot.querySelector('igc-chat-input').shadowRoot.querySelector('igc-textarea'); +} + +function getChatMessages(chat: IgcChatComponent): HTMLElement[] { + return Array.from(chat.renderRoot.querySelectorAll('igc-chat-message')); +} + +function getChatMessageDOM(message: HTMLElement) { + return message.shadowRoot; +}