Skip to content

Commit 45b4637

Browse files
committed
fix(elements): Handle template setting of components passes as event args through proxy. #15615
1 parent dd4a4c5 commit 45b4637

File tree

2 files changed

+87
-4
lines changed

2 files changed

+87
-4
lines changed

projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
IgcGridStateComponent,
1313
} from './components';
1414
import { defineComponents } from '../utils/register';
15+
import { html } from 'lit-html';
1516

1617
describe('Elements: ', () => {
1718
let testContainer: HTMLDivElement;
@@ -103,6 +104,29 @@ describe('Elements: ', () => {
103104
expect(paginator.totalRecords).toEqual(gridEl.data.length);
104105
});
105106

107+
it(`should correctly apply column template when set through event`, async () => {
108+
const gridEl = document.createElement("igc-grid");
109+
110+
const columnID = document.createElement("igc-column");
111+
columnID.setAttribute("field", "ProductID");
112+
gridEl.appendChild(columnID);
113+
const columnName = document.createElement("igc-column");
114+
columnName.setAttribute("field", "ProductName");
115+
gridEl.appendChild(columnName);
116+
117+
gridEl.data = SampleTestData.foodProductData();
118+
gridEl.addEventListener("columnInit", (args: CustomEvent<any>) => {
119+
args.detail.headerTemplate = (ctx) => html`<span>Templated ${args.detail.field}</span>`;
120+
});
121+
testContainer.appendChild(gridEl);
122+
123+
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
124+
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
125+
126+
const header = document.getElementsByTagName("igx-grid-header").item(0) as HTMLElement;
127+
expect(header.innerText).toEqual('Templated ProductID');
128+
});
129+
106130
it(`should initialize pivot grid with state persistence component`, async () => {
107131
const gridEl = document.createElement("igc-pivot-grid");
108132

projects/igniteui-angular-elements/src/app/custom-strategy.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, Injector, OnChanges, QueryList, Type, ViewContainerRef, reflectComponentType } from '@angular/core';
2-
import { NgElement } from '@angular/elements';
3-
import { fromEvent } from 'rxjs';
4-
import { takeUntil } from 'rxjs/operators';
1+
import { ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, QueryList, Type, ViewContainerRef, reflectComponentType } from '@angular/core';
2+
import { NgElement, NgElementStrategyEvent } from '@angular/elements';
3+
import { fromEvent, Observable } from 'rxjs';
4+
import { map, takeUntil } from 'rxjs/operators';
55
import { ComponentConfig, ContentQueryMeta } from './component-config';
66

77
import { ComponentNgElementStrategy, ComponentNgElementStrategyFactory, extractProjectableNodes, isFunction } from './ng-element-strategy';
@@ -388,6 +388,65 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
388388
super.disconnect();
389389
}
390390
}
391+
392+
//#region Handle event args that return reference to components, since they return angular ref and not custom elements.
393+
/** Sets up listeners for the component's outputs so that the events stream emits the events. */
394+
protected override initializeOutputs(componentRef: ComponentRef<any>): void {
395+
const eventEmitters: Observable<NgElementStrategyEvent>[] = this._componentFactory.outputs.map(
396+
({ propName, templateName }) => {
397+
const emitter: EventEmitter<any> = componentRef.instance[propName];
398+
return emitter.pipe(map((value: any) => ({ name: templateName, value: this.patchOutputComponents(value) })));
399+
},
400+
);
401+
402+
(this as any).eventEmitters.next(eventEmitters);
403+
}
404+
405+
protected patchOutputComponents(eventArgs: any) {
406+
let componentConfig: ComponentConfig = this.findConfig(eventArgs);
407+
if (componentConfig?.templateProps) {
408+
// The event return directly reference to a component, so directly return a proxy of it.
409+
eventArgs = this.createElementsComponentProxy(eventArgs, componentConfig);
410+
} else if (!componentConfig) {
411+
// Check if the event args have property with component reference and replace it with proxy as well.
412+
for (const [key, value] of Object.entries(eventArgs)) {
413+
componentConfig = this.findConfig(value);
414+
if (componentConfig?.templateProps) {
415+
eventArgs[key] = this.createElementsComponentProxy(value, componentConfig);
416+
}
417+
}
418+
}
419+
return eventArgs;
420+
}
421+
422+
/** Find config for a component, assuming the provided object is correct. Otherwise will return undefined. */
423+
protected findConfig(component: any) {
424+
// Make sure we match the correct type(first half) and not the one it inherits(second half of check).
425+
return this.config.find((info: ComponentConfig) => component instanceof info.component && !(component.__proto__ instanceof info.component));
426+
}
427+
428+
/** Create proxy for a component that handles setting template props, making sure it provides correct TemplateRef and not Lit template */
429+
protected createElementsComponentProxy(component: any, config: ComponentConfig) {
430+
const parentThis = this;
431+
return new Proxy(component, {
432+
set(target: any, prop: string, newValue: any) {
433+
// For now handle only template props
434+
if (config.templateProps.includes(prop)) {
435+
const oldRef = target[prop];
436+
const oldValue = oldRef && parentThis.templateWrapper.getTemplateFunction(oldRef);
437+
if (oldValue === newValue) {
438+
newValue = oldRef;
439+
} else {
440+
newValue = parentThis.templateWrapper.addTemplate(newValue);
441+
}
442+
}
443+
target[prop] = newValue;
444+
445+
return true;
446+
}
447+
});
448+
}
449+
//#endregion
391450
}
392451

393452
/**

0 commit comments

Comments
 (0)