diff --git a/src/components/grid.ts b/src/components/grid.ts index 8cf4f2f..a37bb87 100644 --- a/src/components/grid.ts +++ b/src/components/grid.ts @@ -182,9 +182,6 @@ export class IgcGridLite extends EventEmitterBase[] = []; - private _initialFilterExpressions: FilterExpression[] = []; - private _updateObservers(): void { this._stateProvider.updateObservers(); } @@ -249,7 +246,9 @@ export class IgcGridLite extends EventEmitterBase extends EventEmitterBase extends EventEmitterBase extends EventEmitterBase | FilterExpression[]): 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); + } } /** diff --git a/src/controllers/filter.ts b/src/controllers/filter.ts index 1c955e2..33c0661 100644 --- a/src/controllers/filter.ts +++ b/src/controllers/filter.ts @@ -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'; @@ -151,4 +151,34 @@ export class FilterController 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[]) { + 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; + } + } + } + } } diff --git a/src/controllers/state.ts b/src/controllers/state.ts index e4e7dbb..61c9f8f 100644 --- a/src/controllers/state.ts +++ b/src/controllers/state.ts @@ -81,6 +81,7 @@ class StateController implements ReactiveController { public setColumnConfiguration(columns: ColumnConfiguration[]): void { this._columns = columns.map((column) => createColumnConfiguration(column)); + this.filtering.resolveConditions(); this._observersCallback.call(this.host); this.host.requestUpdate(PIPELINE); } @@ -88,6 +89,7 @@ class StateController implements ReactiveController { 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); } } diff --git a/test/grid.properties.test.ts b/test/grid.properties.test.ts index 03ade09..3f0779a 100644 --- a/test/grid.properties.test.ts +++ b/test/grid.properties.test.ts @@ -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'; @@ -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 extends GridTestFixture { + public sortState: SortingExpression[] = [{ key: 'id', direction: 'descending' }]; + public filterState: FilterExpression[] = [ + { key: 'importance', condition: 'equals', searchTerm: 'high' }, + ]; + + public override setupTemplate() { + return html` + + `; + } + + 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()); @@ -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); + }); +});