Skip to content

Commit 571e29e

Browse files
authored
refactor: grid internals and controllers (#80)
Reduce duplication and indirection across /src with no behavior or public API changes: - Factor the shared emit -> commit -> emit flow in FilterController into a single helper. - Collapse the six navigation handlers into two clamped movement primitives. - Extract the slotted-column scan in the grid, shared by slot change and column detection. - Add resolveCondition() to replace the repeated operand lookup casts in grid, filter controller and filter row. - Unify the active/inactive chip rendering in the filter row. - Drop needless indirection: pipeline getters, tree iterator generator, sort direction Map, first-render branch in partMap. - Flatten nested ternaries in header sort rendering and the filter row state preview; extract a shared adopt-styles predicate.
1 parent bdeb272 commit 571e29e

14 files changed

Lines changed: 217 additions & 286 deletions

File tree

src/components/filter-row.ts

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import { GRID_STATE_CONTEXT } from '../internal/context.js';
1919
import { registerComponent } from '../internal/register.js';
2020
import { GRID_FILTER_ROW_TAG } from '../internal/tags.js';
2121
import type { ColumnConfiguration, Keys, PropertyType } from '../internal/types.js';
22-
import { getFilterOperandsFor } from '../internal/utils.js';
22+
import { getFilterOperandsFor, resolveCondition } from '../internal/utils.js';
2323
import { watch } from '../internal/watch.js';
2424
import type { FilterExpressionTree } from '../operations/filter/tree.js';
25-
import type { FilterExpression, FilterOperation, OperandKeys } from '../operations/filter/types.js';
25+
import type { FilterExpression, FilterOperation } from '../operations/filter/types.js';
2626
import { styles } from '../styles/filter-row/filter-row.css.js';
2727
import { all } from '../styles/themes/filtering-row-themes.js';
2828

@@ -126,18 +126,18 @@ export default class IgcFilterRow<T extends object> extends LitElement {
126126
});
127127
}
128128

129+
private get _shouldAdoptStyles(): boolean {
130+
return this.adoptRootStyles && this.column.headerTemplate != null;
131+
}
132+
129133
private _handleThemeChange() {
130134
this._adoptedStylesController.invalidateCache(this.ownerDocument);
131-
this._adoptedStylesController.shouldAdoptStyles(
132-
this.adoptRootStyles && this.column.headerTemplate != null
133-
);
135+
this._adoptedStylesController.shouldAdoptStyles(this._shouldAdoptStyles);
134136
}
135137

136138
protected override update(props: PropertyValues<this>): void {
137139
if (props.has('adoptRootStyles')) {
138-
this._adoptedStylesController.shouldAdoptStyles(
139-
this.adoptRootStyles && this.column.headerTemplate != null
140-
);
140+
this._adoptedStylesController.shouldAdoptStyles(this._shouldAdoptStyles);
141141
}
142142

143143
super.update(props);
@@ -160,10 +160,8 @@ export default class IgcFilterRow<T extends object> extends LitElement {
160160

161161
#handleConditionChanged(event: CustomEvent<IgcDropdownItemComponent>) {
162162
event.stopPropagation();
163-
const key = event.detail.value as OperandKeys<PropertyType<T, typeof this.column.field>>;
164163

165-
// XXX: Types
166-
const condition = (getFilterOperandsFor(this.column) as any)[key] as FilterOperation<
164+
const condition = resolveCondition(this.column, event.detail.value) as FilterOperation<
167165
PropertyType<T, keyof T>
168166
>;
169167

@@ -334,23 +332,27 @@ export default class IgcFilterRow<T extends object> extends LitElement {
334332
`;
335333
}
336334

335+
/** Chip list for a column's filter state. `onSelectFor` builds each chip's select action. */
336+
#renderChips(
337+
state: FilterExpressionTree<T>,
338+
onSelectFor: (expression: FilterExpression<T>) => (e: Event) => Promise<void>
339+
) {
340+
return Array.from(state).map((expression, idx) => {
341+
const props: ExpressionChipProps<T> = {
342+
expression,
343+
selected: this.active && this.expression === expression,
344+
onRemove: this.#chipRemoveFor(expression),
345+
onSelect: onSelectFor(expression),
346+
};
347+
348+
return html`${this.renderCriteriaButton(expression, idx)}${this.renderExpressionChip(props)}`;
349+
});
350+
}
351+
337352
protected renderActiveChips() {
338353
const state = this.filterController.get(this.column.field);
339354

340-
return !state
341-
? nothing
342-
: Array.from(state).map((expression, idx) => {
343-
const props: ExpressionChipProps<T> = {
344-
expression,
345-
selected: this.expression === expression,
346-
onRemove: this.#chipRemoveFor(expression),
347-
onSelect: this.#chipSelectFor(expression),
348-
};
349-
350-
return html`${this.renderCriteriaButton(expression, idx)}${this.renderExpressionChip(
351-
props
352-
)}`;
353-
});
355+
return state ? this.#renderChips(state, (expr) => this.#chipSelectFor(expr)) : nothing;
354356
}
355357

356358
protected renderFilterActions() {
@@ -443,44 +445,37 @@ export default class IgcFilterRow<T extends object> extends LitElement {
443445
}
444446

445447
protected renderInactiveChips(column: ColumnConfiguration<T>, state: FilterExpressionTree<T>) {
446-
return Array.from(state).map((expression, idx) => {
447-
const props: ExpressionChipProps<T> = {
448-
expression,
449-
selected: false,
450-
onRemove: this.#chipRemoveFor(expression),
451-
onSelect: async (e: Event) => {
452-
e.stopPropagation();
453-
this.column = column;
454-
this.expression = expression;
455-
this.#show();
456-
},
457-
};
458-
459-
return html`${this.renderCriteriaButton(expression, idx)}${this.renderExpressionChip(props)}`;
448+
return this.#renderChips(state, (expression) => async (e: Event) => {
449+
e.stopPropagation();
450+
this.column = column;
451+
this.expression = expression;
452+
this.#show();
460453
});
461454
}
462455

463456
protected renderFilterState(column: ColumnConfiguration<T>) {
464457
const state = this.filterController.get(column.field);
465458

466-
const partial = state && state.length < MAX_PREVIEW_CHIPS;
467-
const hidden = state && state.length >= MAX_PREVIEW_CHIPS;
459+
// A short expression list renders inline. A longer one collapses into a
460+
// single chip carrying the expression count.
461+
if (state && state.length < MAX_PREVIEW_CHIPS) {
462+
return this.renderInactiveChips(column, state);
463+
}
468464

469465
const open = () => {
470466
this.column = column;
471467
this.#setDefaultExpression();
472468
this.#show();
473469
};
474470

475-
const count = hidden ? html`<span slot="suffix">${state.length}</span>` : nothing;
476-
const chip = html`<igc-chip
471+
const count = state ? html`<span slot="suffix">${state.length}</span>` : nothing;
472+
473+
return html`<igc-chip
477474
data-column=${column.field}
478475
aria-label=${`Filter ${this.#nameFor(column.field)}`}
479476
@click=${open}
480477
>${prefixedIcon('filter')}Filter${count}</igc-chip
481478
>`;
482-
483-
return partial ? this.renderInactiveChips(column, state) : chip;
484479
}
485480

486481
protected renderInactiveState() {

src/components/grid.ts

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type {
2222
Keys,
2323
NavigateToOptions,
2424
} from '../internal/types.js';
25-
import { asArray, getFilterOperandsFor, isNumber, isString } from '../internal/utils.js';
25+
import { asArray, isNumber, isString, resolveCondition } from '../internal/utils.js';
2626
import { watch } from '../internal/watch.js';
2727
import type { FilterExpression } from '../operations/filter/types.js';
2828
import type { SortingExpression } from '../operations/sort/types.js';
@@ -36,19 +36,6 @@ import IgcGridLiteHeaderRow from './header-row.js';
3636
import IgcGridLiteRow from './row.js';
3737
import IgcVirtualizer from './virtualizer.js';
3838

39-
/** Column reducer matching either a direct column element or every one nested in a container */
40-
function columnReducer<T extends Element>(acc: T[], el: T): T[] {
41-
const tag = IgcGridLiteColumn.tagName;
42-
43-
if (el.matches(tag)) {
44-
acc.push(el);
45-
return acc;
46-
}
47-
48-
acc.push(...(Array.from(el.querySelectorAll(tag)) as unknown as T[]));
49-
return acc;
50-
}
51-
5239
/**
5340
* Event object for the filtering event of the grid.
5441
*/
@@ -439,22 +426,23 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
439426
});
440427
}
441428

442-
private _hasAssignedColumns(): boolean {
429+
/** The column elements assigned to the slot, either direct or nested in a container. */
430+
private _assignedColumns(): Element[] {
443431
const slot = this.renderRoot.querySelector('slot') as HTMLSlotElement;
444-
const assignedNodes = slot
432+
const tag = IgcGridLiteColumn.tagName;
433+
434+
return slot
445435
.assignedElements({ flatten: true })
446-
.reduce<Element[]>(columnReducer, []);
447-
return assignedNodes.length > 0;
436+
.flatMap((el) => (el.matches(tag) ? el : Array.from(el.querySelectorAll(tag))));
448437
}
449438

450-
private _handleSlotChange(event: Event): void {
451-
const slot = event.target as HTMLSlotElement;
452-
const assignedNodes = slot
453-
.assignedElements({ flatten: true })
454-
.reduce<Element[]>(columnReducer, []);
439+
private _hasAssignedColumns(): boolean {
440+
return this._assignedColumns().length > 0;
441+
}
455442

443+
private _handleSlotChange(): void {
456444
this._stateController.setColumnConfiguration(
457-
assignedNodes as unknown as ColumnConfiguration<T>[]
445+
this._assignedColumns() as unknown as ColumnConfiguration<T>[]
458446
);
459447
}
460448

@@ -468,10 +456,9 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
468456
.map((expr) => ({ ...expr }));
469457

470458
for (const expr of expressions) {
471-
if (!isString(expr.condition)) {
472-
continue;
459+
if (isString(expr.condition)) {
460+
expr.condition = resolveCondition(this.getColumn(expr.key)!, expr.condition);
473461
}
474-
expr.condition = (getFilterOperandsFor(this.getColumn(expr.key)!) as any)[expr.condition];
475462
}
476463

477464
this._stateController.filtering.filter(expressions);
@@ -513,9 +500,8 @@ export class IgcGridLite<T extends object = any> extends EventEmitterBase<IgcGri
513500
* Returns a {@link ColumnConfiguration} for a given column.
514501
*/
515502
public getColumn(id: Keys<T> | number): ColumnConfiguration<T> | undefined {
516-
return this._stateController.columns.find((column, index) =>
517-
isNumber(id) ? index === id : column.field === id
518-
);
503+
const columns = this._stateController.columns;
504+
return isNumber(id) ? columns[id] : columns.find((column) => column.field === id);
519505
}
520506

521507
@eventOptions({ capture: true })

src/components/header.ts

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ export default class IgcGridLiteHeader<T extends object> extends LitElement {
7777
});
7878
}
7979

80+
private get _shouldAdoptStyles(): boolean {
81+
return this.adoptRootStyles && this.column.headerTemplate != null;
82+
}
83+
8084
protected override update(props: PropertyValues<this>): void {
8185
if (props.has('adoptRootStyles') || props.has('column')) {
82-
this._adoptedStylesController.shouldAdoptStyles(
83-
this.adoptRootStyles && this.column.headerTemplate != null
84-
);
86+
this._adoptedStylesController.shouldAdoptStyles(this._shouldAdoptStyles);
8587
}
8688

8789
// Sort state reaches the header as a context ping, not a property change, so
@@ -102,9 +104,7 @@ export default class IgcGridLiteHeader<T extends object> extends LitElement {
102104

103105
private _handleThemeChange() {
104106
this._adoptedStylesController.invalidateCache(this.ownerDocument);
105-
this._adoptedStylesController.shouldAdoptStyles(
106-
this.adoptRootStyles && this.column.headerTemplate != null
107-
);
107+
this._adoptedStylesController.shouldAdoptStyles(this._shouldAdoptStyles);
108108
}
109109

110110
#addResizeEventHandlers() {
@@ -156,28 +156,27 @@ export default class IgcGridLiteHeader<T extends object> extends LitElement {
156156

157157
protected renderSortPart() {
158158
const state = this.state.sorting.state.get(this.column.field);
159-
const idx = Array.from(this.state.sorting.state.values()).indexOf(state!);
160-
const attr =
161-
this.state.host.sortingOptions.mode === 'multiple' ? (idx > -1 ? idx + 1 : nothing) : nothing;
162-
const icon = state
163-
? state.direction === 'ascending'
164-
? SORT_ICON_ASCENDING
165-
: SORT_ICON_DESCENDING
166-
: SORT_ICON_ASCENDING;
167-
168-
return state || this.isSortable
169-
? html`<span
170-
part=${partMap({ action: true, sorted: !!state?.direction })}
171-
@click=${this.isSortable ? this.#handleClick : nothing}
172-
>
173-
<igc-icon
174-
part=${partMap({ 'sorting-action': !!state })}
175-
data-sortIndex=${attr}
176-
name=${icon}
177-
collection="internal"
178-
></igc-icon>
179-
</span>`
180-
: nothing;
159+
160+
if (!(state || this.isSortable)) {
161+
return nothing;
162+
}
163+
164+
// The 1-based position of the column in the multi-sort order.
165+
const position = Array.from(this.state.sorting.state.keys()).indexOf(this.column.field);
166+
const multiple = this.state.host.sortingOptions.mode === 'multiple';
167+
const icon = state?.direction === 'descending' ? SORT_ICON_DESCENDING : SORT_ICON_ASCENDING;
168+
169+
return html`<span
170+
part=${partMap({ action: true, sorted: !!state?.direction })}
171+
@click=${this.isSortable ? this.#handleClick : nothing}
172+
>
173+
<igc-icon
174+
part=${partMap({ 'sorting-action': !!state })}
175+
data-sortIndex=${multiple && position > -1 ? position + 1 : nothing}
176+
name=${icon}
177+
collection="internal"
178+
></igc-icon>
179+
</span>`;
181180
}
182181

183182
protected renderContentPart() {

src/controllers/data-operation.ts

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { ReactiveController } from 'lit';
2-
import { isDefined } from '../internal/is-defined.js';
32
import type { GridHost } from '../internal/types.js';
43
import FilterDataOperation from '../operations/filter.js';
54
import SortDataOperation from '../operations/sort.js';
@@ -15,35 +14,17 @@ class DataOperationsController<T extends object> implements ReactiveController {
1514

1615
public hostConnected() {}
1716

18-
protected get hasCustomSort() {
19-
return isDefined(this.host.dataPipelineConfiguration?.sort);
20-
}
21-
22-
protected get hasCustomFilter() {
23-
return isDefined(this.host.dataPipelineConfiguration?.filter);
24-
}
25-
26-
protected get customFilter() {
27-
return this.host.dataPipelineConfiguration!.filter!;
28-
}
29-
30-
protected get customSort() {
31-
return this.host.dataPipelineConfiguration!.sort!;
32-
}
33-
3417
public async apply(data: T[], state: StateController<T>) {
35-
const { filtering, sorting } = state;
36-
let transformed: T[];
37-
38-
transformed = this.hasCustomFilter
39-
? await this.customFilter({ data, grid: this.host, type: 'filter' })
40-
: this.filtering.apply(data, filtering.state);
18+
// A hook, when configured, replaces the built-in operation.
19+
const { filter, sort } = this.host.dataPipelineConfiguration ?? {};
4120

42-
transformed = this.hasCustomSort
43-
? await this.customSort({ data: transformed, grid: this.host, type: 'sort' })
44-
: this.sorting.apply(transformed, sorting.state);
21+
const filtered = filter
22+
? await filter({ data, grid: this.host, type: 'filter' })
23+
: this.filtering.apply(data, state.filtering.state);
4524

46-
return transformed;
25+
return sort
26+
? await sort({ data: filtered, grid: this.host, type: 'sort' })
27+
: this.sorting.apply(filtered, state.sorting.state);
4728
}
4829
}
4930

0 commit comments

Comments
 (0)