Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 19 additions & 22 deletions src/components/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,6 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
}) as any,
});

private _initialSortExpressions: SortingExpression<T>[] = [];
private _initialFilterExpressions: FilterExpression<T>[] = [];

private _updateObservers(): void {
this._stateProvider.updateObservers();
}
Expand Down Expand Up @@ -249,7 +246,9 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
if (this.hasUpdated && expressions.length) {
this.sort(expressions);
} else {
this._initialSortExpressions = expressions;
for (const expr of expressions) {
this._stateController.sorting.state.set(expr.key, { ...expr });
}
}
}

Expand All @@ -268,7 +267,7 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
if (this.hasUpdated && expressions.length) {
this.filter(expressions);
} else {
this._initialFilterExpressions = expressions;
this._stateController.filtering.setRaw(expressions);
}
}

Expand Down Expand Up @@ -344,14 +343,6 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
if (this.autoGenerate && !this._hasAssignedColumns()) {
this._stateController.setAutoColumnConfiguration();
}

if (this._initialFilterExpressions.length) {
this.filter(this._initialFilterExpressions);
}

if (this._initialSortExpressions.length) {
this.sort(this._initialSortExpressions);
}
});
}

Expand All @@ -378,15 +369,21 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
* Performs a filter operation in the grid based on the passed expression(s).
*/
public filter(config: FilterExpression<T> | FilterExpression<T>[]): void {
this._stateController.filtering.filter(
asArray(config).map((each) =>
isString(each.condition)
? Object.assign(each, {
condition: (getFilterOperandsFor(this.getColumn(each.key)!) as any)[each.condition],
})
: each
)
);
const expressions = asArray(config).filter((expr) => {
const column = this.getColumn(expr.key);
return column !== undefined;
});

for (const expr of expressions) {
if (!isString(expr.condition)) {
continue;
}
expr.condition = (getFilterOperandsFor(this.getColumn(expr.key)!) as any)[expr.condition];
}

if (expressions.length) {
this._stateController.filtering.filter(expressions);
}
}

/**
Expand Down
32 changes: 31 additions & 1 deletion src/controllers/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type IgcFilterRow from '../components/filter-row.js';
import type { IgcFilteredEvent } from '../components/grid.js';
import { PIPELINE } from '../internal/constants.js';
import type { ColumnConfiguration, Keys } from '../internal/types.js';
import { asArray, getFilterOperandsFor } from '../internal/utils.js';
import { asArray, getFilterOperandsFor, isString } from '../internal/utils.js';
import { FilterState } from '../operations/filter/state.js';
import type { FilterExpression } from '../operations/filter/types.js';
import type { StateController } from './state.js';
Expand Down Expand Up @@ -151,4 +151,34 @@ export class FilterController<T extends object> implements ReactiveController {
)
);
}

/**
* Stores expressions directly in the filter state without requiring column configuration.
* Used when setting initial filter expressions before columns are available.
*/
public setRaw(expressions: FilterExpression<T>[]) {
for (const expr of expressions) {
this.state.set(expr);
}
}

/**
* Resolves any string conditions in the current filter state using available column configuration.
* Called after columns become available to finalize deferred expressions.
*/
public resolveConditions() {
for (const tree of this.state.values) {
const column = this.host.getColumn(tree.key);
if (!column) continue;
const defaults = this.getDefaultExpression(column);
for (const expr of tree.all) {
if (isString(expr.condition)) {
(expr as any).condition = (getFilterOperandsFor(column) as any)[expr.condition as string];
}
if (expr.caseSensitive === undefined) {
expr.caseSensitive = defaults.caseSensitive;
}
}
}
}
}
2 changes: 2 additions & 0 deletions src/controllers/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,15 @@ class StateController<T extends object> implements ReactiveController {

public setColumnConfiguration(columns: ColumnConfiguration<T>[]): void {
this._columns = columns.map((column) => createColumnConfiguration(column));
this.filtering.resolveConditions();
this._observersCallback.call(this.host);
this.host.requestUpdate(PIPELINE);
}

public setAutoColumnConfiguration(): void {
if (this.host.autoGenerate && this.host.data.length > 0) {
this._columns = setColumnsFromData(this.host.data[0]);
this.filtering.resolveConditions();
this.host.requestUpdate(PIPELINE);
}
}
Expand Down
82 changes: 81 additions & 1 deletion test/grid.properties.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, html } from '@open-wc/testing';
import { elementUpdated, expect, html, nextFrame } from '@open-wc/testing';
import { GRID_COLUMN_TAG } from '../src/internal/tags.js';
import type { FilterExpression } from '../src/operations/filter/types.js';
import type { SortingExpression } from '../src/operations/sort/types.js';
import GridTestFixture from './utils/grid-fixture.js';
Expand Down Expand Up @@ -57,6 +58,42 @@ const TDD = new GridTestFixture(data);
const dataStateTDD = new InitialDataStateFixture(data);
const autoGenerateTDD = new AutoGenerateFixture(data);

/**
* Fixture covering the edge case where sort/filter expressions are set on the grid
* before any column elements are slotted. The grid is created without columns, and columns
* are appended (slotted) dynamically after creation.
*/
class LateColumnSlottingFixture<T extends TestData> extends GridTestFixture<T> {
public sortState: SortingExpression<TestData>[] = [{ key: 'id', direction: 'descending' }];
public filterState: FilterExpression<TestData>[] = [
{ key: 'importance', condition: 'equals', searchTerm: 'high' },
];

public override setupTemplate() {
return html`
<igc-grid-lite
.data=${this.data}
.sortingExpressions=${this.sortState}
.filterExpressions=${this.filterState}
></igc-grid-lite>
`;
}

public async slotColumns(columnConfig = this.columnConfig) {
for (const col of columnConfig) {
const elem = Object.assign(document.createElement(GRID_COLUMN_TAG), {
field: col.field,
dataType: col.dataType ?? 'string',
filterable: col.filterable ?? false,
sortable: col.sortable ?? false,
});
this.grid.appendChild(elem);
}
await Promise.all([elementUpdated(this.grid), nextFrame]);
await nextFrame();
}
}

describe('Grid auto-generate column configuration', () => {
const keys = new Set(Object.keys(testData[0]));
beforeEach(async () => await autoGenerateTDD.setUp());
Expand Down Expand Up @@ -129,3 +166,46 @@ describe('Grid properties', () => {
expect(TDD.grid.filterExpressions).lengthOf(3);
});
});

describe('Grid properties (late column slotting)', () => {
const lateSlotTDD = new LateColumnSlottingFixture(data);

beforeEach(async () => await lateSlotTDD.setUp());
afterEach(() => lateSlotTDD.tearDown());

it('filterExpressions getter returns stored expressions before columns are slotted', () => {
expect(lateSlotTDD.grid.filterExpressions).lengthOf(lateSlotTDD.filterState.length);
});

it('sortingExpressions getter returns stored expressions before columns are slotted', () => {
expect(lateSlotTDD.grid.sortingExpressions).lengthOf(lateSlotTDD.sortState.length);
});

it('filter is applied to data after columns are slotted', async () => {
// Data is unfiltered before columns arrive
expect(lateSlotTDD.grid.totalItems).to.equal(data.length);

await lateSlotTDD.slotColumns([{ field: 'importance', dataType: 'string', filterable: true }]);

// Only 'high' importance rows
expect(lateSlotTDD.grid.totalItems).to.equal(
data.filter((d) => d.importance === 'high').length
);
for (const row of lateSlotTDD.grid.rows) {
expect(row.data!.importance).to.equal('high');
}
});

it('sort is applied to data after columns are slotted', async () => {
await lateSlotTDD.slotColumns([{ field: 'id' }]);

// Sorted descending by id — first row should have the highest id
expect(lateSlotTDD.rows.first.data.id).to.equal(Math.max(...data.map((d) => d.id)));
});

it('filterExpressions getter still returns expressions after columns are slotted', async () => {
await lateSlotTDD.slotColumns([{ field: 'importance', dataType: 'string', filterable: true }]);

expect(lateSlotTDD.grid.filterExpressions).lengthOf(lateSlotTDD.filterState.length);
});
});
Loading