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;
+}