Skip to content

Commit d7a169a

Browse files
authored
feat: add nested path support for column fields (#28)
1 parent b40a154 commit d7a169a

20 files changed

Lines changed: 427 additions & 82 deletions

File tree

demo/demo.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,16 @@ type User = {
3030
priority: string;
3131
email: string;
3232
avatar: string;
33+
address: {
34+
city: string;
35+
country: string;
36+
};
3337
};
3438

3539
const choices = ['low', 'standard', 'high'];
3640
const themes = ['bootstrap', 'material', 'fluent', 'indigo'];
41+
const cities = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', 'London', 'Paris', 'Berlin', 'Tokyo', 'Sydney'];
42+
const countries = ['USA', 'USA', 'USA', 'USA', 'USA', 'UK', 'France', 'Germany', 'Japan', 'Australia'];
3743

3844
function getElement<T>(qs: string): T {
3945
return document.querySelector(qs) as T;
@@ -42,8 +48,9 @@ function getElement<T>(qs: string): T {
4248
function generateData(length: number): User[] {
4349
return Array.from(
4450
{ length },
45-
(_, idx) =>
46-
({
51+
(_, idx) => {
52+
const cityIndex = getRandomInt(cities.length);
53+
return {
4754
id: idx,
4855
name: `User - ${getRandomInt(length)}`,
4956
age: getRandomInt(100),
@@ -52,7 +59,12 @@ function generateData(length: number): User[] {
5259
priority: oneOf(choices),
5360
email: `user${idx}@org.com`,
5461
avatar: getAvatar(),
55-
}) as User,
62+
address: {
63+
city: cities[cityIndex],
64+
country: countries[cityIndex],
65+
},
66+
} as User;
67+
},
5668
);
5769
}
5870

@@ -162,6 +174,18 @@ const columns: ColumnConfiguration<User>[] = [
162174
{
163175
field: 'email',
164176
},
177+
{
178+
field: 'address.city',
179+
header: 'City',
180+
sortable: true,
181+
filterable: true,
182+
},
183+
{
184+
field: 'address.country',
185+
header: 'Country',
186+
sortable: true,
187+
filterable: true,
188+
},
165189
{
166190
field: 'subscribed',
167191
dataType: 'boolean',

src/components/column.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
ColumnSortConfiguration,
1010
IgcCellContext,
1111
IgcHeaderContext,
12+
Keys,
1213
} from '../internal/types.js';
1314

1415
/**
@@ -39,7 +40,7 @@ export class IgcGridLiteColumn<T extends object>
3940

4041
/** The field from the data for this column. */
4142
@property()
42-
public field!: keyof T;
43+
public field!: Keys<T>;
4344

4445
/** The data type of the column's values. */
4546
@property()

src/components/filter-row.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { DEFAULT_COLUMN_CONFIG } from '../internal/constants.js';
1313
import { GRID_STATE_CONTEXT } from '../internal/context.js';
1414
import { registerComponent } from '../internal/register.js';
1515
import { GRID_FILTER_ROW_TAG } from '../internal/tags.js';
16-
import type { ColumnConfiguration } from '../internal/types.js';
16+
import type { ColumnConfiguration, PropertyType } from '../internal/types.js';
1717
import { getFilterOperandsFor } from '../internal/utils.js';
1818
import { watch } from '../internal/watch.js';
1919
import type { FilterExpressionTree } from '../operations/filter/tree.js';
@@ -99,11 +99,11 @@ export default class IgcFilterRow<T extends object> extends LitElement {
9999

100100
#handleConditionChanged(event: CustomEvent<IgcDropdownItemComponent>) {
101101
event.stopPropagation();
102-
const key = event.detail.value as OperandKeys<T[typeof this.column.field]>;
102+
const key = event.detail.value as OperandKeys<PropertyType<T, typeof this.column.field>>;
103103

104104
// XXX: Types
105105
this.expression.condition = (getFilterOperandsFor(this.column) as any)[key] as FilterOperation<
106-
T[keyof T]
106+
PropertyType<T, keyof T>
107107
>;
108108

109109
if (this.input.value || this.expression.condition.unary) {

src/components/row.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { map } from 'lit/directives/map.js';
44
import { registerComponent } from '../internal/register.js';
55
import { GRID_ROW_TAG } from '../internal/tags.js';
66
import type { ActiveNode, ColumnConfiguration } from '../internal/types.js';
7+
import { resolveFieldValue } from '../internal/utils.js';
78
import { styles } from '../styles/body-row/body-row.css.js';
89
import IgcGridLiteCell from './cell.js';
910

@@ -51,7 +52,7 @@ export default class IgcGridLiteRow<T extends object> extends LitElement {
5152
.active=${key === column.field && index === this.index}
5253
.column=${column}
5354
.row=${this as IgcGridLiteRow<T>}
54-
.value=${data[column.field]}
55+
.value=${resolveFieldValue(data, column.field)}
5556
></igc-grid-lite-cell>`
5657
)}
5758
`;

src/controllers/filter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class FilterController<T extends object> implements ReactiveController {
8181
public getDefaultExpression(column: ColumnConfiguration<T>) {
8282
const caseSensitive = Boolean(column.filteringCaseSensitive);
8383
const operands = getFilterOperandsFor(column);
84-
const keys = Object.keys(operands) as Keys<typeof operands>[];
84+
const keys = Object.keys(operands) as (keyof typeof operands)[];
8585

8686
// XXX: Types
8787
return {

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export type {
99
BaseColumnConfiguration,
1010
BaseColumnSortConfiguration,
1111
BaseIgcCellContext,
12-
BasePropertyType,
1312
ColumnConfiguration,
1413
ColumnSortConfiguration,
1514
DataPipelineConfiguration,

src/internal/types.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,36 @@ import type { SortComparer } from '../operations/sort/types.js';
88
export type NavigationState = 'previous' | 'current';
99
export type GridHost<T extends object> = ReactiveControllerHost & IgcGridLite<T>;
1010

11+
type FlatKeys<T> = keyof T;
12+
type DotPaths<T> = {
13+
[K in keyof T & string]: T[K] extends object
14+
? K | `${K}.${DotPaths<T[K]>}` // Note: resolving `never` will collapse the entire interpolated string to never, leaving only valid paths
15+
: K;
16+
}[keyof T & string];
17+
type NestedKeys<T> = Exclude<DotPaths<T>, FlatKeys<T>>;
18+
1119
/**
1220
* Helper type for resolving keys of type T.
1321
*/
14-
export type Keys<T> = keyof T;
22+
export type Keys<T> = FlatKeys<T> | NestedKeys<T>;
1523

16-
/**
17-
* Helper type for resolving types of type T.
18-
*/
19-
export type BasePropertyType<T, K extends Keys<T> = Keys<T>> = T[K];
24+
/** Recursive T[K] property type resolve with nested dot paths support */
25+
type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
26+
? K extends keyof T
27+
? PathValue<T[K], Rest>
28+
: never
29+
: P extends keyof T
30+
? T[P]
31+
: never;
2032

2133
/**
2234
* Helper type for resolving types of type T.
2335
*/
24-
export type PropertyType<T, K extends Keys<T> = Keys<T>> = K extends Keys<T>
25-
? BasePropertyType<T, K>
26-
: never;
36+
export type PropertyType<T, K extends Keys<T> = Keys<T>> = K extends NestedKeys<T>
37+
? PathValue<T, K> // nested path
38+
: K extends keyof T
39+
? T[K] // flat key
40+
: never;
2741

2842
/** The data for the current column. */
2943
export type DataType = 'number' | 'string' | 'boolean';

src/internal/utils.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,28 @@ import type { StyleInfo } from 'lit/directives/style-map.js';
22
import { BooleanOperands } from '../operations/filter/operands/boolean.js';
33
import { NumberOperands } from '../operations/filter/operands/number.js';
44
import { StringOperands } from '../operations/filter/operands/string.js';
5-
import type { ColumnConfiguration } from './types.js';
5+
import type { ColumnConfiguration, Keys, PropertyType } from './types.js';
6+
7+
function _isObject(entity: unknown): entity is Record<string, unknown> {
8+
return entity != null && typeof entity === 'object';
9+
}
10+
11+
/**
12+
* Resolves a value from an object using a path string.
13+
* Supports nested properties using dot notation (e.g., 'prop.nestedProp').
14+
*
15+
* @param obj - The object to resolve the value from.
16+
* @param path - The path to the property, can be a simple key or dot-separated path.
17+
* @returns The resolved value, or undefined if the path cannot be resolved.
18+
*/
19+
export function resolveFieldValue<T>(obj: T, path: Keys<T>): PropertyType<T> {
20+
if (typeof path === 'string' && path.includes('.')) {
21+
return path.split('.').reduce<unknown>((current, key) => {
22+
return _isObject(current) && key in current ? current[key] : undefined;
23+
}, obj) as PropertyType<T>;
24+
}
25+
return obj[path as keyof T] as PropertyType<T>;
26+
}
627

728
export function applyColumnWidths<T extends object>(
829
columns: Array<ColumnConfiguration<T>>

src/operations/base.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type { Keys } from '../internal/types.js';
1+
import type { Keys, PropertyType } from '../internal/types.js';
2+
import { resolveFieldValue } from '../internal/utils.js';
23

34
export default abstract class DataOperation<T, K extends Keys<T> = Keys<T>> {
45
protected resolveValue(record: T, key: K) {
5-
return record[key];
6+
return resolveFieldValue(record, key);
67
}
78

8-
protected resolveCase<U = T[K]>(value: U, caseSensitive?: boolean) {
9+
protected resolveCase<U = PropertyType<T, K>>(value: U, caseSensitive?: boolean) {
910
return typeof value === 'string' && !caseSensitive ? (value.toLowerCase() as U) : value;
1011
}
1112

src/operations/filter/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Keys } from '../../internal/types.js';
1+
import type { Keys, PropertyType } from '../../internal/types.js';
22
import type { BooleanOperands } from './operands/boolean.js';
33
import type { NumberOperands } from './operands/number.js';
44
import type { StringOperands } from './operands/string.js';
@@ -38,15 +38,15 @@ export interface BaseFilterExpression<T, K extends Keys<T> = Keys<T>> {
3838
/**
3939
* The filter function which will be executed against the data records.
4040
*/
41-
condition: FilterOperation<T[K]> | OperandKeys<T[K]>;
41+
condition: FilterOperation<PropertyType<T, K>> | OperandKeys<PropertyType<T, K>>;
4242

4343
/**
4444
* The filtering value used in the filter condition function.
4545
*
4646
* @remarks
4747
* Optional for unary conditions.
4848
*/
49-
searchTerm?: T[K];
49+
searchTerm?: PropertyType<T, K>;
5050
/**
5151
* Dictates how this expression should resolve in the filter operation in relation to
5252
* other expressions.

0 commit comments

Comments
 (0)