Skip to content

Commit 91e241c

Browse files
committed
feat: Added chat template directives
1 parent 23490fb commit 91e241c

File tree

2 files changed

+123
-24
lines changed

2 files changed

+123
-24
lines changed

projects/igniteui-angular/src/lib/chat/chat.component.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ChangeDetectionStrategy,
33
Component,
44
CUSTOM_ELEMENTS_SCHEMA,
5+
Directive,
56
effect,
67
inject,
78
input,
@@ -59,11 +60,12 @@ export type NgChatTemplates = {
5960

6061
export type NgChatOptions = Omit<IgcChatOptions, 'renderers'>;
6162

63+
6264
@Component({
6365
selector: 'igx-chat',
6466
changeDetection: ChangeDetectionStrategy.OnPush,
6567
schemas: [CUSTOM_ELEMENTS_SCHEMA],
66-
templateUrl: './chat.component.html'
68+
templateUrl: './chat.component.html',
6769
})
6870
export class IgxChatComponent implements OnInit, OnDestroy {
6971
//#region Internal state
@@ -118,15 +120,14 @@ export class IgxChatComponent implements OnInit, OnDestroy {
118120
// Templates changed - update transformed templates and viewRefs and merge with options
119121
effect(() => {
120122
const templates = this.templates();
121-
this._setTemplates(templates);
122-
123+
this._setTemplates(templates ?? {});
123124
this._mergeOptions(untracked(() => this.options()));
124125
});
125126

126127
// Options changed - merge with current template state
127128
effect(() => {
128129
const options = this.options();
129-
this._mergeOptions(options);
130+
this._mergeOptions(options ?? {});
130131
});
131132
}
132133

@@ -205,3 +206,36 @@ export class IgxChatComponent implements OnInit, OnDestroy {
205206
}
206207
}
207208
}
209+
210+
export interface ChatTemplateContext<T> {
211+
$implicit: T;
212+
}
213+
214+
interface ChatInputContext {
215+
$implicit: string;
216+
attachments: IgcChatMessageAttachment[];
217+
}
218+
219+
@Directive({ selector: '[igxChatMessageContext]' })
220+
export class IgxChatMessageContextDirective {
221+
222+
public static ngTemplateContextGuard(_: IgxChatMessageContextDirective, ctx: unknown): ctx is ChatTemplateContext<IgcChatMessage> {
223+
return true;
224+
}
225+
};
226+
227+
@Directive({ selector: '[igxChatAttachmentContext]' })
228+
export class IgxChatAttachmentContextDirective {
229+
230+
public static ngTemplateContextGuard(_: IgxChatAttachmentContextDirective, ctx: unknown): ctx is ChatTemplateContext<IgcChatMessageAttachment> {
231+
return true;
232+
}
233+
}
234+
235+
@Directive({ selector: '[igxChatInputContext]' })
236+
export class IgxChatInputContextDirective {
237+
238+
public static ngTemplateContextGuard(_: IgxChatInputContextDirective, ctx: unknown): ctx is ChatInputContext {
239+
return true;
240+
}
241+
}

projects/igniteui-angular/src/lib/chat/chat.spec.ts

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'
2-
import { IgxChatComponent } from './chat.component'
2+
import { IgxChatComponent, IgxChatMessageContextDirective, NgChatTemplates } from './chat.component'
33
import { Component, signal, TemplateRef, viewChild } from '@angular/core';
4-
import type { IgcChatMessage } from 'igniteui-webcomponents';
4+
import type { IgcChatComponent, IgcChatMessage, IgcTextareaComponent } from 'igniteui-webcomponents';
55

66
describe('Chat wrapper', () => {
7-
function getShadowRoot(element: HTMLElement) {
8-
return element.shadowRoot;
9-
}
107

118
let chatComponent: IgxChatComponent;
12-
let chatElement: HTMLElement;
9+
let chatElement: IgcChatComponent;
1310
let fixture: ComponentFixture<IgxChatComponent>;
1411

1512
beforeEach(waitForAsync(() => {
@@ -21,7 +18,7 @@ describe('Chat wrapper', () => {
2118
beforeEach(() => {
2219
fixture = TestBed.createComponent(IgxChatComponent);
2320
chatComponent = fixture.componentInstance;
24-
chatElement = (fixture.nativeElement as HTMLElement).querySelector('igc-chat');
21+
chatElement = getChatElement(fixture);
2522
fixture.detectChanges();
2623
})
2724

@@ -43,9 +40,10 @@ describe('Chat wrapper', () => {
4340
fixture.detectChanges();
4441
await fixture.whenStable();
4542

46-
const messageElement = getShadowRoot(chatElement).querySelector<HTMLElement>('igc-chat-message');
43+
44+
const messageElement = getChatMessages(chatElement)[0];
4745
expect(messageElement).toBeDefined();
48-
expect(getShadowRoot(messageElement).textContent.trim()).toEqual(chatComponent.messages()[0].text);
46+
expect(getChatMessageDOM(messageElement).textContent.trim()).toEqual(chatComponent.messages()[0].text);
4947
});
5048

5149
it('correct bindings for draft message', async () => {
@@ -54,29 +52,25 @@ describe('Chat wrapper', () => {
5452
fixture.detectChanges();
5553
await fixture.whenStable();
5654

57-
const textarea = getShadowRoot(getShadowRoot(chatElement).querySelector('igc-chat-input')).querySelector('igc-textarea');
55+
const textarea = getChatInput(chatElement);
5856
expect(textarea.value).toEqual(chatComponent.draftMessage().text);
5957
});
6058
});
6159

6260
describe('Chat templates', () => {
63-
function getShadowRoot(element: HTMLElement) {
64-
return element.shadowRoot;
65-
}
66-
6761
let fixture: ComponentFixture<ChatTemplatesBed>;
68-
let chatElement: HTMLElement;
62+
let chatElement: IgcChatComponent;
6963

7064
beforeEach(waitForAsync(() => {
7165
TestBed.configureTestingModule({
72-
imports: [IgxChatComponent, ChatTemplatesBed]
66+
imports: [IgxChatComponent, IgxChatMessageContextDirective, ChatTemplatesBed]
7367
}).compileComponents();
7468
}));
7569

7670
beforeEach(() => {
7771
fixture = TestBed.createComponent(ChatTemplatesBed);
7872
fixture.detectChanges();
79-
chatElement = (fixture.nativeElement as HTMLElement).querySelector('igc-chat');
73+
chatElement = getChatElement(fixture);
8074
});
8175

8276
it('has correct initially bound template', async () => {
@@ -87,20 +81,49 @@ describe('Chat templates', () => {
8781
// This is so we don't explicitly invoke `viewRef.detectChanges()` inside the returned closure
8882
// from the wrapper's `_createTemplateRenderer` call.
8983
fixture.detectChanges();
90-
expect(getShadowRoot(getShadowRoot(chatElement).querySelector('igc-chat-message')).textContent.trim())
84+
expect(getChatMessageDOM(getChatMessages(chatElement)[0]).textContent.trim())
85+
.toEqual(`Your message: ${fixture.componentInstance.messages()[0].text}`);
86+
});
87+
});
88+
89+
describe('Chat dynamic templates binding', () => {
90+
let fixture: ComponentFixture<ChatDynamicTemplatesBed>;
91+
let chatElement: IgcChatComponent;
92+
93+
beforeEach(waitForAsync(() => {
94+
TestBed.configureTestingModule({
95+
imports: [IgxChatComponent, IgxChatMessageContextDirective, ChatDynamicTemplatesBed]
96+
}).compileComponents();
97+
}));
98+
99+
beforeEach(() => {
100+
fixture = TestBed.createComponent(ChatDynamicTemplatesBed);
101+
fixture.detectChanges();
102+
chatElement = getChatElement(fixture);
103+
});
104+
105+
it('supports late binding', async () => {
106+
fixture.componentInstance.bindTemplates();
107+
fixture.detectChanges();
108+
109+
await fixture.whenStable();
110+
fixture.detectChanges();
111+
112+
expect(getChatMessageDOM(getChatMessages(chatElement)[0]).textContent.trim())
91113
.toEqual(`Your message: ${fixture.componentInstance.messages()[0].text}`);
92114
});
115+
93116
});
94117

95118

96119
@Component({
97120
template: `
98121
<igx-chat [messages]="messages()" [templates]="{messageContent: messageTemplate()}"/>
99-
<ng-template #message let-message>
122+
<ng-template igxChatMessageContext #message let-message>
100123
<h3>Your message: {{ message.text }}</h3>
101124
</ng-template>
102125
`,
103-
imports: [IgxChatComponent]
126+
imports: [IgxChatComponent, IgxChatMessageContextDirective]
104127
})
105128
class ChatTemplatesBed {
106129
public messages = signal<IgcChatMessage[]>([{
@@ -110,3 +133,45 @@ class ChatTemplatesBed {
110133
}]);
111134
public messageTemplate = viewChild.required<TemplateRef<any>>('message');
112135
}
136+
137+
@Component({
138+
template: `
139+
<igx-chat [messages]="messages()" [templates]="templates()" />
140+
<ng-template igxChatMessageContext #message let-message>
141+
<h3>Your message: {{ message.text }}</h3>
142+
</ng-template>
143+
`,
144+
imports: [IgxChatComponent, IgxChatMessageContextDirective]
145+
})
146+
class ChatDynamicTemplatesBed {
147+
public templates = signal<NgChatTemplates | null>(null);
148+
public messages = signal<IgcChatMessage[]>([{
149+
id: '1',
150+
sender: 'user',
151+
text: 'Hello world'
152+
}]);
153+
public messageTemplate = viewChild.required<TemplateRef<any>>('message');
154+
155+
public bindTemplates(): void {
156+
this.templates.set({
157+
messageContent: this.messageTemplate()
158+
});
159+
}
160+
}
161+
162+
function getChatElement<T>(fixture: ComponentFixture<T>): IgcChatComponent {
163+
const nativeElement = fixture.nativeElement as HTMLElement;
164+
return nativeElement.querySelector('igc-chat');
165+
}
166+
167+
function getChatInput(chat: IgcChatComponent): IgcTextareaComponent {
168+
return chat.renderRoot.querySelector('igc-chat-input').shadowRoot.querySelector('igc-textarea');
169+
}
170+
171+
function getChatMessages(chat: IgcChatComponent): HTMLElement[] {
172+
return Array.from(chat.renderRoot.querySelectorAll('igc-chat-message'));
173+
}
174+
175+
function getChatMessageDOM(message: HTMLElement) {
176+
return message.shadowRoot;
177+
}

0 commit comments

Comments
 (0)