Skip to content

Commit 05db2ef

Browse files
authored
Merge branch '19.2.x' into dkamburov/expose-change
2 parents 51e2f2b + 27faabd commit 05db2ef

File tree

13 files changed

+289
-17
lines changed

13 files changed

+289
-17
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ name: "CodeQL"
1313

1414
on:
1515
push:
16-
branches: [ master, 19.1.x, 18.2.x, 17.2.x, 16.1.x, 15.1.x ]
16+
branches: [ master, 19.2.x, 18.2.x, 17.2.x, 16.1.x, 15.1.x ]
1717
pull_request:
1818
# The branches below must be a subset of the branches above
19-
branches: [ master, 19.1.x, 18.2.x, 17.2.x, 16.1.x, 15.1.x ]
19+
branches: [ master, 19.2.x, 18.2.x, 17.2.x, 16.1.x, 15.1.x ]
2020
schedule:
2121
- cron: '33 4 * * 4'
2222

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { IgxColumnComponent, IgxGridComponent, IgxHierarchicalGridComponent } from 'igniteui-angular';
2+
import { html } from 'lit-html';
23
import { firstValueFrom, fromEvent, skip, timer } from 'rxjs';
34
import { ComponentRefKey, IgcNgElement } from './custom-strategy';
45
import hgridData from '../assets/data/projects-hgrid.js';
@@ -52,6 +53,43 @@ describe('Elements: ', () => {
5253
const columnComponent = (await columnEl.ngElementStrategy[ComponentRefKey]).instance as IgxColumnComponent;
5354
expect(gridComponent.columnList.toArray()).toContain(columnComponent);
5455
});
56+
57+
it(`should keep IgcNgElement instance in template of another IgcNgElement #15678`, async () => {
58+
const gridEl = document.createElement("igc-grid");
59+
testContainer.appendChild(gridEl);
60+
const columnEl = document.createElement("igc-column") as IgcNgElement;
61+
gridEl.appendChild(columnEl);
62+
gridEl.primaryKey = 'id';
63+
gridEl.data = [{ id: '1' }];
64+
(gridEl as any).detailTemplate = (ctx) => {
65+
return html`<div>
66+
<igc-grid id="child${ctx.implicit.id}"></igc-grid>
67+
</div>`;
68+
}
69+
70+
// TODO: Better way to wait - potentially expose the queue or observable for update on the strategy
71+
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
72+
73+
// sigh (。﹏。*)
74+
(gridEl as any).toggleRow('1');
75+
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
76+
77+
let detailGrid = document.querySelector<IgcNgElement>('#child1');
78+
expect(detailGrid).toBeDefined();
79+
let detailGridComponent = (await detailGrid?.ngElementStrategy[ComponentRefKey])?.instance as IgxGridComponent;
80+
expect(detailGridComponent).toBeDefined();
81+
82+
// close and re-expand row detail:
83+
(gridEl as any).toggleRow('1');
84+
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
85+
(gridEl as any).toggleRow('1');
86+
await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2));
87+
88+
detailGrid = document.querySelector<IgcNgElement>('#child1');
89+
expect(detailGrid).toBeDefined();
90+
detailGridComponent = (await detailGrid?.ngElementStrategy[ComponentRefKey])?.instance as IgxGridComponent;
91+
expect(detailGridComponent).toBeDefined("Detail child grid was destroyed on re-expand");
92+
});
5593
});
5694

5795
describe('Grid integration scenarios.', () => {
@@ -103,6 +141,29 @@ describe('Elements: ', () => {
103141
expect(paginator.totalRecords).toEqual(gridEl.data.length);
104142
});
105143

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+
106167
it(`should initialize pivot grid with state persistence component`, async () => {
107168
const gridEl = document.createElement("igc-pivot-grid");
108169

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

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
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, DestroyRef, EventEmitter, Injector, OnChanges, QueryList, Type, ViewContainerRef, reflectComponentType } from '@angular/core';
2+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
3+
import { NgElement, NgElementStrategyEvent } from '@angular/elements';
4+
import { fromEvent, Observable } from 'rxjs';
5+
import { map, takeUntil } from 'rxjs/operators';
56
import { ComponentConfig, ContentQueryMeta } from './component-config';
67

78
import { ComponentNgElementStrategy, ComponentNgElementStrategyFactory, extractProjectableNodes, isFunction } from './ng-element-strategy';
@@ -28,6 +29,8 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
2829
/** Cached child instances per query prop. Used for dynamic components's child templates that normally persist in Angular runtime */
2930
protected cachedChildComponents: Map<string, ComponentRef<any>[]> = new Map();
3031
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;
3134

3235
/**
3336
* Resolvable component reference.
@@ -48,6 +51,14 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
4851
return this._templateWrapper;
4952
}
5053

54+
private _configSelectors: string;
55+
public get configSelectors(): string {
56+
if (!this._configSelectors) {
57+
this._configSelectors = this.config.map(x => x.selector).join(',');
58+
}
59+
return this._configSelectors;
60+
}
61+
5162
constructor(private _componentFactory: ComponentFactory<any>, private _injector: Injector, private config: ComponentConfig[]) {
5263
super(_componentFactory, _injector);
5364
}
@@ -233,6 +244,14 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
233244
}
234245
value = this.templateWrapper.addTemplate(value);
235246
// TODO: discard oldValue
247+
248+
// check template for any angular-element components
249+
this.templateWrapper.templateRendered.pipe(takeUntilDestroyed(componentRef.injector.get(DestroyRef))).subscribe((element) => {
250+
element.querySelectorAll<IgcNgElement>(this.configSelectors)?.forEach((c) => {
251+
// tie to angularParent lifecycle for cached scenarios like detailTemplate:
252+
c.ngElementStrategy.angularParent = componentRef;
253+
});
254+
});
236255
}
237256
if (componentRef && componentConfig?.boolProps?.includes(property)) {
238257
// bool coerce:
@@ -388,6 +407,94 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy {
388407
super.disconnect();
389408
}
390409
}
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
391498
}
392499

393500
/**

projects/igniteui-angular-elements/src/app/wrapper/wrapper.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ChangeDetectorRef, Component, QueryList, TemplateRef, ViewChildren } from '@angular/core';
2+
import { Subject } from 'rxjs';
23
import { TemplateRefWrapper } from './template-ref-wrapper';
34

45
import { render, TemplateResult } from 'lit-html';
@@ -14,6 +15,7 @@ type TemplateFunction = (arg: any) => TemplateResult;
1415
export class TemplateWrapperComponent {
1516

1617
public templateFunctions: TemplateFunction[] = [];
18+
public templateRendered = new Subject<HTMLElement>();
1719

1820
/**
1921
* All template refs
@@ -27,6 +29,7 @@ export class TemplateWrapperComponent {
2729

2830
public litRender(container: HTMLElement, templateFunc: (arg: any) => TemplateResult, arg: any) {
2931
render(templateFunc(arg), container);
32+
this.templateRendered.next(container);
3033
}
3134

3235
public addTemplate(templateFunc: TemplateFunction): TemplateRef<any> {

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,11 @@ <h3 class="ig-typography__h6">Flat Grid (MRL column layout)</h3>
195195
<igc-combo ${ref(focusCallback)} style="width:100%; height:100%" .data=${northwindProducts} value-key="ProductName" @igcChange=${(e) => ctx.cell.editValue = e.detail.newValue} single-select>
196196
</igc-combo>
197197
`;
198-
grid1.detailTemplate = (ctx) => html`<div><span class="categoryStyle">Stock: ${ctx.implicit.InStock}</span></div>`;
198+
grid1.detailTemplate = (ctx) => {
199+
return html`<div>
200+
<igc-grid auto-generate="true"></igc-grid>
201+
</div>`;
202+
}
199203

200204
grid2.querySelector('igc-column[field="ProductName"]').inlineEditorTemplate = (ctx) =>
201205
html`
@@ -320,7 +324,9 @@ <h3 class="ig-typography__h6">Flat Grid (MRL column layout)</h3>
320324
<igc-pivot-grid id="pivotgrid1" default-expand-state="true" row-selection="single"></igc-pivot-grid>
321325

322326
<script src="assets/data/pivot-data.js"></script>
323-
<script>
327+
<script type="module">
328+
import { html, nothing } from "/lit-html.js";
329+
324330
document.addEventListener('DOMContentLoaded', () => {
325331
const clear = (el) => el === 0 || Boolean(el);
326332
pivotgrid1.pivotConfiguration = {
@@ -409,6 +415,10 @@ <h3 class="ig-typography__h6">Flat Grid (MRL column layout)</h3>
409415
};
410416
});
411417
pivotgrid1.data = pivotData;
418+
pivotgrid1.addEventListener("columnInit", (args) => {
419+
const col = args.detail;
420+
col.headerTemplate = (ctx) => html`<span>${col.field}_</span>`;
421+
});
412422
</script>
413423
<!-- END IgxPivotGridComponent -->
414424

projects/igniteui-angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
}
9494
},
9595
"igxDevDependencies": {
96-
"@igniteui/angular-schematics": "~19.1.14315"
96+
"@igniteui/angular-schematics": "~19.2.1440"
9797
},
9898
"ng-update": {
9999
"migrations": "./migrations/migration-collection.json",

projects/igniteui-angular/src/lib/core/styles/components/icon-button/_icon-button-theme.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@
243243
}
244244
}
245245

246-
[igxIconButton='flat'].igx-button--focused {
246+
%igx-icon-button--flat.igx-button--focused {
247247
background: var-get($flat-theme, 'focus-background');
248248
color: var-get($flat-theme, 'focus-foreground');
249249

@@ -388,7 +388,7 @@
388388
}
389389
}
390390

391-
[igxIconButton='outlined'].igx-button--focused {
391+
%igx-icon-button--outlined.igx-button--focused {
392392
background: var-get($outlined-theme, 'focus-background');
393393
color: var-get($outlined-theme, 'focus-foreground');
394394

projects/igniteui-angular/src/lib/core/styles/components/input/_input-group-theme.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,10 @@
686686
position: relative;
687687

688688
grid-area: 1 / 2;
689+
690+
%form-group-label {
691+
color: var-get($theme, 'idle-secondary-color');
692+
}
689693
}
690694

691695
%igx-input-group__notch--border {

projects/igniteui-angular/src/lib/services/csv/char-separated-value-data.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export class CharSeparatedValueData {
5252
this._escapeCharacters.push(this._delimiter);
5353

5454
const headers = columns && columns.length ?
55-
columns.map(c => c.header ?? c.field) :
55+
/* When column groups are present, always use the field as it indicates the group the column belongs to.
56+
* Otherwise, in PivotGrid scenarios we can end up with many duplicated column names without a hint what they represent.
57+
*/
58+
columns.map(c => c.columnGroupParent ? c.field : c.header ?? c.field) :
5659
keys;
5760

5861
this._headerRecord = this.processHeaderRecord(headers, this._data.length);

0 commit comments

Comments
 (0)