Skip to content

Commit e0939f4

Browse files
committed
fix: Old templates refs handling when there is a dynamic change
Added unit tests
1 parent c96c716 commit e0939f4

File tree

2 files changed

+124
-7
lines changed

2 files changed

+124
-7
lines changed

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export class IgxChatComponent implements OnInit, OnDestroy {
7070

7171
private readonly _view = inject(ViewContainerRef);
7272
private readonly _templateViewRefs = new Map<TemplateRef<any>, Set<ViewRef>>();
73+
private _oldTemplates: NgChatTemplates = {};
7374

7475
protected readonly _mergedOptions = signal<IgcChatOptions>({});
7576
protected readonly _transformedTemplates = signal<ChatRenderers>({});
@@ -100,27 +101,29 @@ export class IgxChatComponent implements OnInit, OnDestroy {
100101

101102
//#endregion
102103

104+
/** @internal */
103105
public ngOnInit(): void {
104106
IgcChatComponent.register();
105107
}
106108

109+
/** @internal */
107110
public ngOnDestroy(): void {
108111
for (const viewSet of this._templateViewRefs.values()) {
109-
for (const viewRef of viewSet) {
110-
viewRef.destroy();
111-
}
112+
viewSet.forEach(viewRef => viewRef.destroy());
112113
}
113114
this._templateViewRefs.clear();
114115
}
115116

116117
constructor() {
118+
// Templates changed - update transformed templates and viewRefs and merge with options
117119
effect(() => {
118120
const templates = this.templates();
119121
this._setTemplates(templates);
120122

121123
this._mergeOptions(untracked(() => this.options()));
122124
});
123125

126+
// Options changed - merge with current template state
124127
effect(() => {
125128
const options = this.options();
126129
this._mergeOptions(options);
@@ -140,7 +143,7 @@ export class IgxChatComponent implements OnInit, OnDestroy {
140143
const templateCopies: ChatRenderers = {};
141144
const newTemplateKeys = Object.keys(newTemplates) as Array<keyof NgChatTemplates>;
142145

143-
const oldTemplates = this.templates();
146+
const oldTemplates = this._oldTemplates;
144147
const oldTemplateKeys = Object.keys(oldTemplates) as Array<keyof NgChatTemplates>;
145148

146149
for (const key of oldTemplateKeys) {
@@ -157,9 +160,11 @@ export class IgxChatComponent implements OnInit, OnDestroy {
157160
}
158161

159162
if (newTemplateKeys.length > 0) {
163+
this._oldTemplates = {};
160164
for (const key of newTemplateKeys) {
161165
const ref = newTemplates[key];
162166
if (ref) {
167+
this._oldTemplates[key] = ref as any;
163168
templateCopies[key] = this._createTemplateRenderer(ref);
164169
}
165170
}
@@ -193,10 +198,10 @@ export class IgxChatComponent implements OnInit, OnDestroy {
193198
angularContext = { $implicit: { instance: context.instance } };
194199
}
195200

196-
const node = this._view.createEmbeddedView(ref, angularContext);
197-
viewSet.add(node);
201+
const viewRef = this._view.createEmbeddedView(ref, angularContext);
202+
viewSet.add(viewRef);
198203

199-
return node.rootNodes;
204+
return viewRef.rootNodes;
200205
}
201206
}
202207
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'
2+
import { IgxChatComponent } from './chat.component'
3+
import { Component, signal, TemplateRef, viewChild } from '@angular/core';
4+
import type { IgcChatMessage } from 'igniteui-webcomponents';
5+
6+
describe('Chat wrapper', () => {
7+
function getShadowRoot(element: HTMLElement) {
8+
return element.shadowRoot;
9+
}
10+
11+
let chatComponent: IgxChatComponent;
12+
let chatElement: HTMLElement;
13+
let fixture: ComponentFixture<IgxChatComponent>;
14+
15+
beforeEach(waitForAsync(() => {
16+
TestBed.configureTestingModule({
17+
imports: [IgxChatComponent]
18+
}).compileComponents();
19+
}));
20+
21+
beforeEach(() => {
22+
fixture = TestBed.createComponent(IgxChatComponent);
23+
chatComponent = fixture.componentInstance;
24+
chatElement = (fixture.nativeElement as HTMLElement).querySelector('igc-chat');
25+
fixture.detectChanges();
26+
})
27+
28+
it('is created', () => {
29+
expect(chatComponent).toBeDefined();
30+
});
31+
32+
it('has correct initial empty state', () => {
33+
const draft = chatComponent.draftMessage();
34+
35+
expect(chatComponent.messages().length).toEqual(0);
36+
expect(draft.text).toEqual('');
37+
expect(draft.attachments).toBeUndefined();
38+
});
39+
40+
it('correct bindings for messages', async () => {
41+
fixture.componentRef.setInput('messages', [{ id: '1', sender: 'user', text: 'Hello' }]);
42+
43+
fixture.detectChanges();
44+
await fixture.whenStable();
45+
46+
const messageElement = getShadowRoot(chatElement).querySelector<HTMLElement>('igc-chat-message');
47+
expect(messageElement).toBeDefined();
48+
expect(getShadowRoot(messageElement).textContent.trim()).toEqual(chatComponent.messages()[0].text);
49+
});
50+
51+
it('correct bindings for draft message', async () => {
52+
fixture.componentRef.setInput('draftMessage', { text: 'Hello world' });
53+
54+
fixture.detectChanges();
55+
await fixture.whenStable();
56+
57+
const textarea = getShadowRoot(getShadowRoot(chatElement).querySelector('igc-chat-input')).querySelector('igc-textarea');
58+
expect(textarea.value).toEqual(chatComponent.draftMessage().text);
59+
});
60+
});
61+
62+
describe('Chat templates', () => {
63+
function getShadowRoot(element: HTMLElement) {
64+
return element.shadowRoot;
65+
}
66+
67+
let fixture: ComponentFixture<ChatTemplatesBed>;
68+
let chatElement: HTMLElement;
69+
70+
beforeEach(waitForAsync(() => {
71+
TestBed.configureTestingModule({
72+
imports: [IgxChatComponent, ChatTemplatesBed]
73+
}).compileComponents();
74+
}));
75+
76+
beforeEach(() => {
77+
fixture = TestBed.createComponent(ChatTemplatesBed);
78+
fixture.detectChanges();
79+
chatElement = (fixture.nativeElement as HTMLElement).querySelector('igc-chat');
80+
});
81+
82+
it('has correct initially bound template', async () => {
83+
await fixture.whenStable();
84+
85+
// NOTE: This is invoked since in the test bed there is no app ref so fresh embedded view
86+
// has no change detection ran on it. In an application scenario this is not the case.
87+
// This is so we don't explicitly invoke `viewRef.detectChanges()` inside the returned closure
88+
// from the wrapper's `_createTemplateRenderer` call.
89+
fixture.detectChanges();
90+
expect(getShadowRoot(getShadowRoot(chatElement).querySelector('igc-chat-message')).textContent.trim())
91+
.toEqual(`Your message: ${fixture.componentInstance.messages()[0].text}`);
92+
});
93+
});
94+
95+
96+
@Component({
97+
template: `
98+
<igx-chat [messages]="messages()" [templates]="{messageContent: messageTemplate()}"/>
99+
<ng-template #message let-message>
100+
<h3>Your message: {{ message.text }}</h3>
101+
</ng-template>
102+
`,
103+
imports: [IgxChatComponent]
104+
})
105+
class ChatTemplatesBed {
106+
public messages = signal<IgcChatMessage[]>([{
107+
id: '1',
108+
sender: 'user',
109+
text: 'Hello world'
110+
}]);
111+
public messageTemplate = viewChild.required<TemplateRef<any>>('message');
112+
}

0 commit comments

Comments
 (0)