Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
28eec6b
feat(h-grid): update adv filtering schema on row expand
igdmdimitrov Jun 11, 2025
3cc7307
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
gedinakova Jun 30, 2025
28f8a35
fix(adv-filtering): improve child entities fields population on row e…
igdmdimitrov Jul 4, 2025
d923eaa
chore(*): add deprecation of schema property to changelog
igdmdimitrov Jul 16, 2025
e89ab5e
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
igdmdimitrov Oct 7, 2025
2568b77
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
igdmdimitrov Oct 7, 2025
a418259
chore(*): update deprecation comment
igdmdimitrov Oct 7, 2025
5974cb3
Merge branch 'dmdimitrov/hgrid-adv-filtering-schema' of https://githu…
igdmdimitrov Oct 7, 2025
fa96ebd
Apply suggestions from code review
igdmdimitrov Oct 14, 2025
4501c70
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
gedinakova Oct 14, 2025
0be78a3
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
igdmdimitrov Mar 11, 2026
913063e
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
igdmdimitrov Jul 30, 2026
2ce115e
Merge branch 'master' of https://github.com/IgniteUI/igniteui-angular…
kdinev Nov 9, 2026
36d111f
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
kdinev Nov 10, 2026
120a16a
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
kdinev Nov 10, 2026
7ef19e9
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
kdinev Nov 11, 2026
c804e00
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
ChronosSF Nov 11, 2026
26a8fb5
fix(h-grid): sync generated schema with column changes
igdmdimitrov Nov 11, 2026
eb2dc93
Merge branch 'master' into dmdimitrov/hgrid-adv-filtering-schema
ChronosSF Nov 11, 2026
1b45c06
fix(h-grid): keep adv filtering schema in sync with columns
igdmdimitrov Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ All notable changes for each version of this project will be documented in this
### General

- **Browser support** - The minimum supported browser versions are now documented: Chrome and Edge 131, Firefox 151, and Safari 18 on macOS and iOS. The component styles rely on CSS container style queries and relative colors, so older browsers, including Firefox ESR 140, are not supported. See the [Browser Support](README.md#browser-support) section of the README.
- `IgxHierarchicalGridComponent`
- **Deprecation** - The `schema` input has been deprecated and will be removed in a future version. Define the columns and the row islands of the grid instead, the advanced filtering schema is generated from them. A `schema` that is set is still used as before.

### New Functionality

Expand Down Expand Up @@ -123,6 +125,11 @@ All notable changes for each version of this project will be documented in this
- Autosizing a child column of a column layout now fits its content, instead of keeping the width of its layout area.
- When grouping is applied and no column is pinned at the start, the group-by indentation and the summary area below it use the pinned border.
- Multi-row layout cell borders stay visible when the row is hovered or selected.
- `IgxHierarchicalGridComponent`
- The advanced filtering dialog now lists the nested row islands of a row island whose child data is not available yet, e.g. when it is loaded on demand. Previously they were left out when the first record had no child data.
- The fields of an auto-generated row island in the advanced filtering dialog are updated when its child grids generate their columns, e.g. when the child data is loaded on demand, and take the column `header`, `dataType` and formatting options set in `columnInit`. The fields of its nested row islands that are still empty are resolved from the loaded child data. Previously they were resolved only from the first record and were empty when it had no child data.
- The fields of a row island with declared columns in the advanced filtering dialog now take the column `header`, `filters`, `editorOptions` and formatting options, as the fields of the root grid do. Previously only the `field` and the `dataType` were used.
- The fields in the advanced filtering dialog are updated when the columns of the root grid or of a row island are added, removed or changed after the dialog was first opened, e.g. when they are rendered conditionally or loaded later.
- `IgxTreeGridComponent`
- Setting `data` before the grid is initialized while `autoGenerate` is enabled no longer throws an error. In Ignite UI for Web Components this happened when `data` was set on `igc-tree-grid` before the element was attached to the page.
- `IgxPivotGridComponent`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1851,6 +1851,49 @@ describe('IgxGrid - Advanced Filtering #grid - ', () => {
expect(dropdownValues).toEqual(expectedValues);
});

it('Should have proper fields in UI when auto-generated child data is loaded on demand without a schema.', async () => {
const fixture = TestBed.createComponent(IgxHGridAutoGenerateOnDemandComponent);
const hierarchicalGrid = fixture.componentInstance.hGrid;
hierarchicalGrid.allowAdvancedFiltering = true;
fixture.detectChanges();

// There is no child data yet, but the child entities are generated from the row islands.
const [childEntity] = hierarchicalGrid.schema[0].childEntities;
expect(childEntity.name).toBe('childData');
expect(childEntity.fields).toEqual([]);
expect(childEntity.childEntities.map(e => e.name)).toEqual(['childData2']);

// Load the child data on demand.
hierarchicalGrid.expandRow('0');
fixture.detectChanges();
await fixture.componentInstance.childDataLoaded;
fixture.detectChanges();
await fixture.whenStable();

hierarchicalGrid.openAdvancedFilteringDialog();
fixture.detectChanges();

// Click the initial 'Add Condition' button.
QueryBuilderFunctions.clickQueryBuilderInitialAddConditionBtn(fixture, 0);
await fixture.whenStable();
fixture.detectChanges();
// Populate edit inputs.
await QueryBuilderFunctions.selectColumnInEditModeExpression(fixture, 0); // Select 'ID' column.
await QueryBuilderFunctions.selectOperatorInEditModeExpression(fixture, 10); // Select 'In' operator.
await fixture.whenStable();
fixture.detectChanges();

const entityInputGroup = QueryBuilderFunctions.getQueryBuilderEntitySelect(fixture, 1).querySelector('input');
expect(entityInputGroup.value).toBe('childData');

// Verify return fields
const queryBuilderElement: HTMLElement = fixture.debugElement.queryAll(By.css(`.${QueryBuilderSelectors.QUERY_BUILDER_TREE}`))[1].nativeElement;
QueryBuilderFunctions.clickQueryBuilderFieldsCombo(fixture, 1);
fixture.detectChanges();
const dropdownValues = QueryBuilderFunctions.getQueryBuilderSelectDropdownItems(queryBuilderElement, 0).map((x: any) => x.innerText);
expect(dropdownValues).toEqual(['ID', 'ProductName', 'Price']);
});

it('Should correctly change resource strings for hierarchical Advanced Filtering dialog.', async () => {
hgrid.closeAdvancedFilteringDialog(false);
await fix.whenStable();
Expand Down Expand Up @@ -2029,6 +2072,37 @@ class IgxHGridRemoteOnDemandComponent {
}
}

@Component({
template: `
<igx-hierarchical-grid #hGrid [data]="data" [primaryKey]="'ID'" [autoGenerate]="true" [height]="'600px'">
<igx-row-island [key]="'childData'" [autoGenerate]="true" (gridCreated)="gridCreated($event)">
<igx-row-island [key]="'childData2'" [autoGenerate]="true"></igx-row-island>
</igx-row-island>
</igx-hierarchical-grid>`,
changeDetection: ChangeDetectionStrategy.Eager,
imports: [IgxHierarchicalGridComponent, IgxRowIslandComponent]
})
class IgxHGridAutoGenerateOnDemandComponent {
@ViewChild('hGrid', { read: IgxHierarchicalGridComponent, static: true })
public hGrid: IgxHierarchicalGridComponent;

public data = [
{ ID: '0', ProductName: 'Product: A0' },
{ ID: '1', ProductName: 'Product: A1' }
];

/** Resolved when the on demand child data is assigned to the created child grid. */
public childDataLoaded: Promise<void>;

public gridCreated(event: IGridCreatedEventArgs) {
this.childDataLoaded = new Promise(resolve => setTimeout(() => {
event.grid.data = [{ ID: 10, ProductName: 'Child: A0', Price: 5 }];
event.grid.cdr.detectChanges();
resolve();
}));
}
}

@Component({
template: `
<igx-hierarchical-grid #hGrid [data]="data" [autoGenerate]="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti
}

private _data: any;
private _schemaSetByUser = false;
private h_id = `igx-hierarchical-grid-${NEXT_ID++}`;
private childGridTemplates: Map<any, any> = new Map();

Expand Down Expand Up @@ -581,16 +582,21 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti
* @Input()
* @param {EntityType[]} entities - An array of EntityType objects representing the grid's schema.
* @remarks
* This property is required in remote data filtering scenarios.
* When not set, the schema is generated from the filterable columns of the grid and from the row islands.
* The fields of auto-generated row islands are resolved from the child data and are updated when
* the child grids generate their columns, e.g. when the child data is loaded on demand.
* @example
* ```typescript
* const schema = this.grid.schema;
* this.grid.schema = [{ name: 'Products', fields: [...], childEntities: [...] }];
* ```
* @deprecated in version 22.3.0. Define the columns and the row islands of the grid instead,
* the schema is generated from them.
*/
@Input()
public set schema(entities: EntityType[]) {
this._hGridSchema = entities;
this._schemaSetByUser = !!entities;
}

Comment thread
kdinev marked this conversation as resolved.
Resolved
/* blazorSuppress */
Expand Down Expand Up @@ -1258,6 +1264,9 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti
this.createColumnsList(this.parentIsland.childColumns.toArray());
} else {
super.setupColumns();
if (this.autoGenerate && this.columns.length > 0) {
this.updateGeneratedSchemaFields();
}
}
}

Expand All @@ -1276,6 +1285,9 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti
protected override onColumnsChanged() {
Promise.resolve().then(() => {
this.updateColumnList();
if (!this.parent) {
this.updateGeneratedSchemaFields();
}
});
}

Expand Down Expand Up @@ -1311,23 +1323,14 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti
}

private generateSchema() {
const filterableFields = this.columns.filter((column) => !column.columnGroup && column.filterable);
const filterableFields = this.getFilterableFields(this.columns);
let entities!: EntityType[];

if(filterableFields.length !== 0) {
entities = [
{
name: null!,
fields: filterableFields.map(f => ({
field: f.field,
dataType: f.dataType,
header: f.header,
editorOptions: f.editorOptions,
filters: f.filters,
pipeArgs: f.pipeArgs,
defaultTimeFormat: f.defaultTimeFormat,
defaultDateTimeFormat: f.defaultDateTimeFormat
})) as FieldType[]
fields: filterableFields
}
];

Expand All @@ -1342,37 +1345,144 @@ export class IgxHierarchicalGridComponent extends IgxHierarchicalGridBaseDirecti
return entities;
}

private getFilterableFields(columns: IgxColumnComponent[]): FieldType[] {
return this.toFieldTypes(columns.filter((column) => !column.columnGroup && column.filterable));
}

private toFieldTypes(columns: IgxColumnComponent[]): FieldType[] {
return columns.map(f => ({
field: f.field,
dataType: f.dataType,
header: f.header,
editorOptions: f.editorOptions,
filters: f.filters,
pipeArgs: f.pipeArgs,
defaultTimeFormat: f.defaultTimeFormat,
defaultDateTimeFormat: f.defaultDateTimeFormat
})) as FieldType[];
}

/**
* Updates the fields of the generated schema entity that corresponds to this grid with its current columns,
* so that columns auto-generated after the schema was created (e.g. for child data loaded on demand)
* become available in the advanced filtering dialog. A schema set by the user is not modified.
*/
private updateGeneratedSchemaFields() {
const root = this.rootGrid as IgxHierarchicalGridComponent;
const fields = this.parentIsland ? this.getChildGridFields(this.columns) : this.getFilterableFields(this.columns);
const entity = root.setGeneratedSchemaFields(this.parentIsland, fields);

@viktorkombov viktorkombov Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Editing a filter in the Advanced Filtering dialog throws after expanding a row whose first child record has null in the filtered field (master is fine).
Data: [{ ID: 1, orders: [{ OrderID: 10, Total: 25 }] }, { ID: 2, orders: [{ OrderID: 20, Total: null }, { OrderID: 21, Total: 40 }] }] in a grid with an ID column and <igx-row-island key="orders" [autoGenerate]="true">.
Steps: apply the filter ID In (orders: Total > 20) → expand row 2 → Advanced Filtering → click the ID In chip → click the Total Greater Than 20 chip → TypeError: Cannot read properties of undefined (reading 'isUnary').
Why: the child grid of row 2 makes Total a string field, and this line replaces the orders fields of all rows with its columns. Maybe add only the missing fields here?

const firstRecord = this.data?.[0];
if (entity && this.parentIsland && firstRecord) {
// nested row islands that have no fields yet (e.g. their parent data is loaded on demand)
// are resolved from the first record of this grid, as they are when the schema is generated
root.fillEmptySchemaFields(entity, root.generateChildEntity(this.parentIsland, firstRecord));
}
}

/** Sets the fields of the entities that have none from the matching entities of another entity tree. */
private fillEmptySchemaFields(target: EntityType, source: EntityType) {
if (!target.fields?.length) {
target.fields = source.fields;
}
source.childEntities?.forEach(sourceChild => {
const targetChild = target.childEntities?.find(e => e.name === sourceChild.name);
if (targetChild) {
this.fillEmptySchemaFields(targetChild, sourceChild);
}
});
}

/**
* Resolves the fields of an auto-generated row island from the columns of one of its child grids.
* Columns that are not filterable are kept, so that expression trees that already use them can still be recreated.
*/
private getChildGridFields(columns: IgxColumnComponent[]): FieldType[] {
return this.toFieldTypes(columns.filter((column) => !column.columnGroup && column.field));
}

/**
* Updates the fields of the generated schema entity that corresponds to a row island with declared columns,
* so that columns added, removed or changed after the schema was created are reflected
* in the advanced filtering dialog. A schema set by the user is not modified.
*
* @hidden @internal
*/
public updateRowIslandSchemaFields(rowIsland: IgxRowIslandComponent) {
if (!rowIsland.autoGenerate) {
this.setGeneratedSchemaFields(rowIsland, this.getRowIslandFields(rowIsland));
}
}

/**
* Sets the fields of the generated schema entity of a row island, or of the root entity when no row island is passed.
* Returns the updated entity, if any.
*/
private setGeneratedSchemaFields(rowIsland: IgxRowIslandComponent | null | undefined, fields: FieldType[]): EntityType | undefined {
if (this._schemaSetByUser || !this._hGridSchema?.length) {
return undefined;
}

const path: string[] = [];
let island = rowIsland;
while (island) {
path.unshift(island.key);
island = island.parentIsland;
}

let entity: EntityType | undefined = this._hGridSchema[0];
for (const key of path) {
entity = entity?.childEntities?.find(e => e.name === key);
}

if (entity) {
entity.fields = fields;

@viktorkombov viktorkombov Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applying a saved advanced filter throws after expanding a row whose first child record has null in the filtered field (master is fine).
Data: [{ ID: 1, orders: [{ OrderID: 10, Total: 25 }] }, { ID: 2, orders: [{ OrderID: 20, Total: null }, { OrderID: 21, Total: 40 }] }] in a grid with an ID column and <igx-row-island key="orders" [autoGenerate]="true">.
Steps: expand row 2 → grid.advancedFilteringExpressionsTree = JSON.parse(saved), where saved is the filter ID In (orders: Total > 20) stored as JSON (only conditionName, no condition) → Wrong conditionName, condition or field provided!.
Why: after the expand Total is a string field, so greaterThan is not found for it.

}
return entity;
}

/**
* Resolves the fields of a row island with declared columns, including the ones in column groups.
* Columns that are not filterable are kept, so that expression trees that already use them can still be recreated.
*/
private getRowIslandFields(rowIsland: IgxRowIslandComponent): FieldType[] {
return this.toFieldTypes(flatten(rowIsland.childColumns.toArray()).filter(col => col.field));
}

private generateChildEntity(rowIsland: IgxRowIslandComponent, firstRowData: any): EntityType {
const entityName = rowIsland.key;
let fields: any[] = [];
let childEntities;
// a child grid that is already created has its columns auto-generated from the actual child data,
// with the header, data type and formatting options set in columnInit
const childGrid = rowIsland.autoGenerate &&
rowIsland.rowIslandAPI.getChildGrids().find(grid => grid.columns.length > 0);
const childGridFields = childGrid ? this.getChildGridFields(childGrid.columns) : [];
if (!rowIsland.autoGenerate) {
fields = flatten(rowIsland.childColumns.toArray()).filter(col => col.field)
.map(f => ({ field: f.field, dataType: f.dataType })) as FieldType[];
fields = this.getRowIslandFields(rowIsland);
} else if (firstRowData) {
const rowIslandFields = Object.keys(firstRowData).map(key => {
if (firstRowData[key] instanceof Array) {
return null;
}

return {
return childGridFields.find(f => f.field === key) ?? {

@viktorkombov viktorkombov Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With no filter applied, the Advanced Filtering dialog shows text conditions for a number field after expanding a row whose first child record has null in it (master is fine).
Data: [{ ID: 1, orders: [{ OrderID: 10, Total: 25 }] }, { ID: 2, orders: [{ OrderID: 20, Total: null }, { OrderID: 21, Total: 40 }] }] in a grid with an ID column and <igx-row-island key="orders" [autoGenerate]="true">.
Steps: expand row 2 → Advanced Filtering → ID → In → orders → Total → the conditions are Contains, Starts With… (master: Greater Than, Less Than…).
Why: the fields are created only when the dialog opens, and this line takes dataType from the child grid of row 2. Maybe keep the type from the first record and take only header and formatting from the column?

field: key,
dataType: this.resolveDataTypes(firstRowData[key])
}
};
});
fields = rowIslandFields.filter(f => f !== null) as FieldType[];
} else {
fields = childGridFields;
}

// the child entities are generated from the row islands even if there is no child data yet (e.g. load on demand)
const rowIslandChildEntities = rowIsland.childLayoutList.reduce((acc, childRowIsland) => {
if (!firstRowData) {
return null;
}
const childFirstRowData = firstRowData[childRowIsland.key]?.length > 0 ?
const childFirstRowData = firstRowData?.[childRowIsland.key]?.length > 0 ?
firstRowData[childRowIsland.key][0] : null;
return acc.concat(this.generateChildEntity(childRowIsland, childFirstRowData));
}, [] as any);
}, [] as EntityType[]);

if (rowIslandChildEntities?.length > 0) {
if (rowIslandChildEntities.length > 0) {
childEntities = rowIslandChildEntities;
}

Expand Down
Loading
Loading