Skip to content

Commit 27faabd

Browse files
authored
fix(elements): Handle template setting of components passed as event args through proxy. (#15649)
1 parent 5f255b2 commit 27faabd

File tree

3 files changed

+124
-5
lines changed

3 files changed

+124
-5
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,29 @@ describe('Elements: ', () => {
141141
expect(paginator.totalRecords).toEqual(gridEl.data.length);
142142
});
143143

144+
it(`should correctly apply column template when set through event`, async () => {
145+
const gridEl = document.createElement("igc-grid");
146+
147+
const columnID = document.createElement("igc-column");
148+
columnID.setAttribute("field", "ProductID");
149+
gridEl.appendChild(columnID);
150+
const columnName = document.createElement("igc-column");
151+
columnName.setAttribute("field", "ProductName");
152+
gridEl.appendChild(columnName);
153+
154+
gridEl.data = SampleTestData.foodProductData();
155+
gridEl.addEventListener("columnInit", (args: CustomEvent<any>) => {
156+
args.detail.headerTemplate = (ctx) => html`<span>Templated ${args.detail.field}</span>`;
157+
});
158+
testContainer.appendChild(gridEl);
159+
160+
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
161+
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
162+
163+
const header = document.getElementsByTagName("igx-grid-header").item(0) as HTMLElement;
164+
expect(header.innerText).toEqual('Templated ProductID');
165+
});
166+
144167
it(`should initialize pivot grid with state persistence component`, async () => {
145168
const gridEl = document.createElement("igc-pivot-grid");
146169

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

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

88
import { ComponentNgElementStrategy, ComponentNgElementStrategyFactory, extractProjectableNodes, isFunction } from './ng-element-strategy';
@@ -29,6 +29,8 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
2929
/** Cached child instances per query prop. Used for dynamic components's child templates that normally persist in Angular runtime */
3030
protected cachedChildComponents: Map<string, ComponentRef<any>[]> = new Map();
3131
private setComponentRef: (value: ComponentRef<any>) => void;
32+
/** The maximum depth at which event arguments are processed and angular components wrapped with Proxies, that handle template set */
33+
private maxEventProxyDepth = 3;
3234

3335
/**
3436
* Resolvable component reference.
@@ -405,6 +407,94 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
405407
super.disconnect();
406408
}
407409
}
410+
411+
//#region Handle event args that return reference to components, since they return angular ref and not custom elements.
412+
/** Sets up listeners for the component's outputs so that the events stream emits the events. */
413+
protected override initializeOutputs(componentRef: ComponentRef<any>): void {
414+
const eventEmitters: Observable<NgElementStrategyEvent>[] = this._componentFactory.outputs.map(
415+
({ propName, templateName }) => {
416+
const emitter: EventEmitter<any> = componentRef.instance[propName];
417+
return emitter.pipe(map((value: any) => ({ name: templateName, value: this.patchOutputComponents(propName, value) })));
418+
},
419+
);
420+
421+
(this as any).eventEmitters.next(eventEmitters);
422+
}
423+
424+
protected patchOutputComponents(eventName: string, eventArgs: any) {
425+
// Single out only `columnInit` event for now. If more events pop up will require a config generation.
426+
if (eventName !== "columnInit") {
427+
return eventArgs;
428+
}
429+
return this.createProxyForComponentValue(eventArgs, 1).value;
430+
}
431+
432+
/**
433+
* Nested search of event args that contain angular components and replace them with proxies.
434+
* If event args are array of angular component instances should return array of proxies of each of those instances.
435+
* If event args are object that has a single property being angular component should return same object except the angular component being a proxy of itself.
436+
*/
437+
protected createProxyForComponentValue(value: any, depth: number): { value: any, hasProxies: boolean } {
438+
if (depth > this.maxEventProxyDepth) {
439+
return { value, hasProxies: false };
440+
}
441+
442+
let hasProxies = false;
443+
// TO DO!: Not very reliable as it is a very internal API and could be subject to change. If something comes up, should be changed.
444+
if (value?.__ngContext__) {
445+
const componentConfig = this.config.find((info: ComponentConfig) => value.constructor === info.component);
446+
if (componentConfig?.templateProps) {
447+
return { value: this.createElementsComponentProxy(value, componentConfig), hasProxies: true };
448+
}
449+
} else if (Array.isArray(value)) {
450+
if (!value[0]) {
451+
return { value, hasProxies: false };
452+
} else {
453+
// For array limit their parsing to first level and check if first item has created proxy inside.
454+
const firstItem = this.createProxyForComponentValue(value[0], this.maxEventProxyDepth);
455+
if (firstItem.hasProxies) {
456+
const mappedArray = value.slice(1, value.length).map(item => this.createProxyForComponentValue(item, depth + 1));
457+
mappedArray.unshift(firstItem);
458+
return { value: mappedArray, hasProxies: true };
459+
}
460+
}
461+
} else if (typeof value === "object" && Object.entries(value).length && !(value instanceof Event)) {
462+
for (const [key, item] of Object.entries(value)) {
463+
if (!item) {
464+
value[key] = item;
465+
} else {
466+
const parsedItem = this.createProxyForComponentValue(item, depth + 1);
467+
value[key] = parsedItem.value;
468+
hasProxies = parsedItem.hasProxies || hasProxies;
469+
}
470+
}
471+
}
472+
473+
return { value, hasProxies };
474+
}
475+
476+
/** Create proxy for a component that handles setting template props, making sure it provides correct TemplateRef and not Lit template */
477+
protected createElementsComponentProxy(component: any, config: ComponentConfig) {
478+
const parentThis = this;
479+
return new Proxy(component, {
480+
set(target: any, prop: string, newValue: any) {
481+
// For now handle only template props
482+
if (config.templateProps.includes(prop)) {
483+
const oldRef = target[prop];
484+
const oldValue = oldRef && parentThis.templateWrapper.getTemplateFunction(oldRef);
485+
if (oldValue === newValue) {
486+
newValue = oldRef;
487+
} else {
488+
newValue = parentThis.templateWrapper.addTemplate(newValue);
489+
}
490+
}
491+
target[prop] = newValue;
492+
493+
return true;
494+
}
495+
});
496+
}
497+
//#endregion
408498
}
409499

410500
/**

projects/igniteui-angular-elements/src/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,9 @@ <h3 class="ig-typography__h6">Flat Grid (MRL column layout)</h3>
324324
<igc-pivot-grid id="pivotgrid1" default-expand-state="true" row-selection="single"></igc-pivot-grid>
325325

326326
<script src="assets/data/pivot-data.js"></script>
327-
<script>
327+
<script type="module">
328+
import { html, nothing } from "/lit-html.js";
329+
328330
document.addEventListener('DOMContentLoaded', () => {
329331
const clear = (el) => el === 0 || Boolean(el);
330332
pivotgrid1.pivotConfiguration = {
@@ -413,6 +415,10 @@ <h3 class="ig-typography__h6">Flat Grid (MRL column layout)</h3>
413415
};
414416
});
415417
pivotgrid1.data = pivotData;
418+
pivotgrid1.addEventListener("columnInit", (args) => {
419+
const col = args.detail;
420+
col.headerTemplate = (ctx) => html`<span>${col.field}_</span>`;
421+
});
416422
</script>
417423
<!-- END IgxPivotGridComponent -->
418424

0 commit comments

Comments
 (0)