Skip to content

Commit 555b4cf

Browse files
authored
fix: Initial filter/sort state without colunn configuration crashes (#55)
Closes #41
1 parent 19a1034 commit 555b4cf

4 files changed

Lines changed: 133 additions & 24 deletions

File tree

src/components/grid.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,6 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
182182
}) as any,
183183
});
184184

185-
private _initialSortExpressions: SortingExpression<T>[] = [];
186-
private _initialFilterExpressions: FilterExpression<T>[] = [];
187-
188185
private _updateObservers(): void {
189186
this._stateProvider.updateObservers();
190187
}
@@ -249,7 +246,9 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
249246
if (this.hasUpdated && expressions.length) {
250247
this.sort(expressions);
251248
} else {
252-
this._initialSortExpressions = expressions;
249+
for (const expr of expressions) {
250+
this._stateController.sorting.state.set(expr.key, { ...expr });
251+
}
253252
}
254253
}
255254

@@ -268,7 +267,7 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
268267
if (this.hasUpdated && expressions.length) {
269268
this.filter(expressions);
270269
} else {
271-
this._initialFilterExpressions = expressions;
270+
this._stateController.filtering.setRaw(expressions);
272271
}
273272
}
274273

@@ -344,14 +343,6 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
344343
if (this.autoGenerate && !this._hasAssignedColumns()) {
345344
this._stateController.setAutoColumnConfiguration();
346345
}
347-
348-
if (this._initialFilterExpressions.length) {
349-
this.filter(this._initialFilterExpressions);
350-
}
351-
352-
if (this._initialSortExpressions.length) {
353-
this.sort(this._initialSortExpressions);
354-
}
355346
});
356347
}
357348

@@ -378,15 +369,21 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
378369
* Performs a filter operation in the grid based on the passed expression(s).
379370
*/
380371
public filter(config: FilterExpression<T> | FilterExpression<T>[]): void {
381-
this._stateController.filtering.filter(
382-
asArray(config).map((each) =>
383-
isString(each.condition)
384-
? Object.assign(each, {
385-
condition: (getFilterOperandsFor(this.getColumn(each.key)!) as any)[each.condition],
386-
})
387-
: each
388-
)
389-
);
372+
const expressions = asArray(config).filter((expr) => {
373+
const column = this.getColumn(expr.key);
374+
return column !== undefined;
375+
});
376+
377+
for (const expr of expressions) {
378+
if (!isString(expr.condition)) {
379+
continue;
380+
}
381+
expr.condition = (getFilterOperandsFor(this.getColumn(expr.key)!) as any)[expr.condition];
382+
}
383+
384+
if (expressions.length) {
385+
this._stateController.filtering.filter(expressions);
386+
}
390387
}
391388

392389
/**

src/controllers/filter.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type IgcFilterRow from '../components/filter-row.js';
33
import type { IgcFilteredEvent } from '../components/grid.js';
44
import { PIPELINE } from '../internal/constants.js';
55
import type { ColumnConfiguration, Keys } from '../internal/types.js';
6-
import { asArray, getFilterOperandsFor } from '../internal/utils.js';
6+
import { asArray, getFilterOperandsFor, isString } from '../internal/utils.js';
77
import { FilterState } from '../operations/filter/state.js';
88
import type { FilterExpression } from '../operations/filter/types.js';
99
import type { StateController } from './state.js';
@@ -151,4 +151,34 @@ export class FilterController<T extends object> implements ReactiveController {
151151
)
152152
);
153153
}
154+
155+
/**
156+
* Stores expressions directly in the filter state without requiring column configuration.
157+
* Used when setting initial filter expressions before columns are available.
158+
*/
159+
public setRaw(expressions: FilterExpression<T>[]) {
160+
for (const expr of expressions) {
161+
this.state.set(expr);
162+
}
163+
}
164+
165+
/**
166+
* Resolves any string conditions in the current filter state using available column configuration.
167+
* Called after columns become available to finalize deferred expressions.
168+
*/
169+
public resolveConditions() {
170+
for (const tree of this.state.values) {
171+
const column = this.host.getColumn(tree.key);
172+
if (!column) continue;
173+
const defaults = this.getDefaultExpression(column);
174+
for (const expr of tree.all) {
175+
if (isString(expr.condition)) {
176+
(expr as any).condition = (getFilterOperandsFor(column) as any)[expr.condition as string];
177+
}
178+
if (expr.caseSensitive === undefined) {
179+
expr.caseSensitive = defaults.caseSensitive;
180+
}
181+
}
182+
}
183+
}
154184
}

src/controllers/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ class StateController<T extends object> implements ReactiveController {
8181

8282
public setColumnConfiguration(columns: ColumnConfiguration<T>[]): void {
8383
this._columns = columns.map((column) => createColumnConfiguration(column));
84+
this.filtering.resolveConditions();
8485
this._observersCallback.call(this.host);
8586
this.host.requestUpdate(PIPELINE);
8687
}
8788

8889
public setAutoColumnConfiguration(): void {
8990
if (this.host.autoGenerate && this.host.data.length > 0) {
9091
this._columns = setColumnsFromData(this.host.data[0]);
92+
this.filtering.resolveConditions();
9193
this.host.requestUpdate(PIPELINE);
9294
}
9395
}

test/grid.properties.test.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { expect, html } from '@open-wc/testing';
1+
import { elementUpdated, expect, html, nextFrame } from '@open-wc/testing';
2+
import { GRID_COLUMN_TAG } from '../src/internal/tags.js';
23
import type { FilterExpression } from '../src/operations/filter/types.js';
34
import type { SortingExpression } from '../src/operations/sort/types.js';
45
import GridTestFixture from './utils/grid-fixture.js';
@@ -57,6 +58,42 @@ const TDD = new GridTestFixture(data);
5758
const dataStateTDD = new InitialDataStateFixture(data);
5859
const autoGenerateTDD = new AutoGenerateFixture(data);
5960

61+
/**
62+
* Fixture covering the edge case where sort/filter expressions are set on the grid
63+
* before any column elements are slotted. The grid is created without columns, and columns
64+
* are appended (slotted) dynamically after creation.
65+
*/
66+
class LateColumnSlottingFixture<T extends TestData> extends GridTestFixture<T> {
67+
public sortState: SortingExpression<TestData>[] = [{ key: 'id', direction: 'descending' }];
68+
public filterState: FilterExpression<TestData>[] = [
69+
{ key: 'importance', condition: 'equals', searchTerm: 'high' },
70+
];
71+
72+
public override setupTemplate() {
73+
return html`
74+
<igc-grid-lite
75+
.data=${this.data}
76+
.sortingExpressions=${this.sortState}
77+
.filterExpressions=${this.filterState}
78+
></igc-grid-lite>
79+
`;
80+
}
81+
82+
public async slotColumns(columnConfig = this.columnConfig) {
83+
for (const col of columnConfig) {
84+
const elem = Object.assign(document.createElement(GRID_COLUMN_TAG), {
85+
field: col.field,
86+
dataType: col.dataType ?? 'string',
87+
filterable: col.filterable ?? false,
88+
sortable: col.sortable ?? false,
89+
});
90+
this.grid.appendChild(elem);
91+
}
92+
await Promise.all([elementUpdated(this.grid), nextFrame]);
93+
await nextFrame();
94+
}
95+
}
96+
6097
describe('Grid auto-generate column configuration', () => {
6198
const keys = new Set(Object.keys(testData[0]));
6299
beforeEach(async () => await autoGenerateTDD.setUp());
@@ -129,3 +166,46 @@ describe('Grid properties', () => {
129166
expect(TDD.grid.filterExpressions).lengthOf(3);
130167
});
131168
});
169+
170+
describe('Grid properties (late column slotting)', () => {
171+
const lateSlotTDD = new LateColumnSlottingFixture(data);
172+
173+
beforeEach(async () => await lateSlotTDD.setUp());
174+
afterEach(() => lateSlotTDD.tearDown());
175+
176+
it('filterExpressions getter returns stored expressions before columns are slotted', () => {
177+
expect(lateSlotTDD.grid.filterExpressions).lengthOf(lateSlotTDD.filterState.length);
178+
});
179+
180+
it('sortingExpressions getter returns stored expressions before columns are slotted', () => {
181+
expect(lateSlotTDD.grid.sortingExpressions).lengthOf(lateSlotTDD.sortState.length);
182+
});
183+
184+
it('filter is applied to data after columns are slotted', async () => {
185+
// Data is unfiltered before columns arrive
186+
expect(lateSlotTDD.grid.totalItems).to.equal(data.length);
187+
188+
await lateSlotTDD.slotColumns([{ field: 'importance', dataType: 'string', filterable: true }]);
189+
190+
// Only 'high' importance rows
191+
expect(lateSlotTDD.grid.totalItems).to.equal(
192+
data.filter((d) => d.importance === 'high').length
193+
);
194+
for (const row of lateSlotTDD.grid.rows) {
195+
expect(row.data!.importance).to.equal('high');
196+
}
197+
});
198+
199+
it('sort is applied to data after columns are slotted', async () => {
200+
await lateSlotTDD.slotColumns([{ field: 'id' }]);
201+
202+
// Sorted descending by id — first row should have the highest id
203+
expect(lateSlotTDD.rows.first.data.id).to.equal(Math.max(...data.map((d) => d.id)));
204+
});
205+
206+
it('filterExpressions getter still returns expressions after columns are slotted', async () => {
207+
await lateSlotTDD.slotColumns([{ field: 'importance', dataType: 'string', filterable: true }]);
208+
209+
expect(lateSlotTDD.grid.filterExpressions).lengthOf(lateSlotTDD.filterState.length);
210+
});
211+
});

0 commit comments

Comments
 (0)