Skip to content

Commit

Permalink
feat(filter): adds ability to match only specific filter data propert…
Browse files Browse the repository at this point in the history
…ies (#9541)

**Related Issue:** #5063

## Summary

- add matchFields argument to filter utility
  - allows filtering fields at the first level of an object.
  - Nested object fields will not be filtered.
- Could be enhanced to do so in the future by enhancing `matchFields` to
be an object
- add filter utility spec tests (thank you co-pilot)
- add `matchFields` property to `filter` component
- add e2e tests for filter
  • Loading branch information
driskull committed Jun 14, 2024
1 parent d15f667 commit 137d9ae
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 10 deletions.
8 changes: 8 additions & 0 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,10 @@ export namespace Components {
* Defines the items to filter. The component uses the values as the starting point, and returns items that contain the string entered in the input, using a partial match and recursive search. This property is needed to conduct filtering.
*/
"items": object[];
/**
* Specifies the fields to match against when filtering.
*/
"matchFields": string[];
/**
* Use this property to override individual strings used by the component.
*/
Expand Down Expand Up @@ -9617,6 +9621,10 @@ declare namespace LocalJSX {
* Defines the items to filter. The component uses the values as the starting point, and returns items that contain the string entered in the input, using a partial match and recursive search. This property is needed to conduct filtering.
*/
"items"?: object[];
/**
* Specifies the fields to match against when filtering.
*/
"matchFields"?: string[];
/**
* Use this property to override individual strings used by the component.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ describe("calcite-filter", () => {
await page.waitForTimeout(DEBOUNCE_TIMEOUT);
assertMatchingItems(await filter.getProperty("filteredItems"), ["harry"]);
});

it("should return no matching values", async () => {
const filter = await page.find("calcite-filter");
filter.setProperty("matchFields", ["description"]);
await page.waitForChanges();
await page.waitForTimeout(DEBOUNCE_TIMEOUT);
assertMatchingItems(await filter.getProperty("filteredItems"), []);
});
});

describe("filter method", () => {
Expand Down
15 changes: 13 additions & 2 deletions packages/calcite-components/src/components/filter/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ export class Filter
*/
@Prop({ mutable: true }) filteredItems: object[] = [];

/**
* Specifies the fields to match against when filtering. This will only apply when `value` is an object. If not set, all fields will be matched.
*/
@Prop() matchFields: string[];

@Watch("matchFields")
matchFieldsHandler(): void {
this.filterDebounced(this.value);
}

/**
* Specifies placeholder text for the input element.
*/
Expand Down Expand Up @@ -159,7 +169,7 @@ export class Filter
async componentWillLoad(): Promise<void> {
setUpLoadableComponent(this);
if (this.items.length) {
this.updateFiltered(filter(this.items, this.value));
this.updateFiltered(filter(this.items, this.value, this.matchFields));
}
await setUpMessages(this);
}
Expand Down Expand Up @@ -223,7 +233,8 @@ export class Filter

private filterDebounced = debounce(
(value: string, emit = false, onFilter?: () => void): void =>
this.items.length && this.updateFiltered(filter(this.items, value), emit, onFilter),
this.items.length &&
this.updateFiltered(filter(this.items, value, this.matchFields), emit, onFilter),
DEBOUNCE_TIMEOUT,
);

Expand Down
71 changes: 71 additions & 0 deletions packages/calcite-components/src/utils/filter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { filter } from "./filter";

describe("filter function", () => {
it("warns and returns empty array for empty data", () => {
const spy = jest.spyOn(console, "warn");
const result = filter([], "test");
expect(spy).toHaveBeenCalledTimes(1);
expect(result).toEqual([]);
spy.mockRestore();
});

it("returns empty array when no objects match", () => {
const data = [{ name: "John" }, { name: "Jane" }];
const result = filter(data, "Doe");
expect(result).toEqual([]);
});

it("returns matching objects", () => {
const data = [{ name: "John" }, { name: "Jane" }];
const result = filter(data, "Jane");
expect(result).toEqual([{ name: "Jane" }]);
});

it("considers only specified fields for matching", () => {
const data = [
{ name: "John", age: 30 },
{ name: "Jane", age: 25 },
];
const result = filter(data, "25", ["age"]);
expect(result).toEqual([{ name: "Jane", age: 25 }]);

const result2 = filter(data, "John", ["age"]);
expect(result2).toEqual([]);
});

it("ignores functions and null values in objects", () => {
const data = [{ name: "John", action: () => {}, value: null }];
const result = filter(data, "John");
expect(result).toEqual([{ name: "John", action: expect.any(Function), value: null }]);
});

it("returns empty array when searching with 'null'", () => {
const data = [{ name: "John", action: () => {}, value: null }];
const result = filter(data, "null");
expect(result).toEqual([]);
});

it("searches nested objects correctly", () => {
const data = [{ name: "John", details: { age: 30 } }];
const result = filter(data, "30");
expect(result).toEqual([{ name: "John", details: { age: 30 } }]);
});

it("searches arrays in objects correctly", () => {
const data = [{ name: "John", tags: ["developer", "tester"] }];
const result = filter(data, "tester");
expect(result).toEqual([{ name: "John", tags: ["developer", "tester"] }]);
});

it("always includes objects with filterDisabled set to true", () => {
const data = [{ name: "John" }, { name: "Jane", filterDisabled: true }];
const result = filter(data, "Doe");
expect(result).toEqual([{ name: "Jane", filterDisabled: true }]);
});

it("always includes objects with constant set to true", () => {
const data = [{ name: "John" }, { name: "Jane", constant: true }];
const result = filter(data, "Doe");
expect(result).toEqual([{ name: "Jane", constant: true }]);
});
});
19 changes: 11 additions & 8 deletions packages/calcite-components/src/utils/filter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { escapeRegExp, forIn } from "lodash-es";

export const filter = (data: Array<object>, value: string): Array<any> => {
export const filter = (data: Array<object>, value: string, matchFields?: string[]): Array<any> => {
const escapedValue = escapeRegExp(value);
const regex = new RegExp(escapedValue, "i");

Expand All @@ -9,16 +9,22 @@ export const filter = (data: Array<object>, value: string): Array<any> => {
The data argument should be an array of objects`);
}

const find = (input: object, RE: RegExp) => {
const find = (input: object, RE: RegExp, fields?: string[]) => {
if ((input as any)?.constant || (input as any)?.filterDisabled) {
return true;
}

let found = false;

forIn(input, (val) => {
forIn(input, (val, key) => {
if (typeof val === "function" || val == null /* intentional == to catch undefined */) {
return;
}

if (fields && !fields.includes(key)) {
return;
}

if (Array.isArray(val) || (typeof val === "object" && val !== null)) {
if (find(val, RE)) {
found = true;
Expand All @@ -27,12 +33,9 @@ export const filter = (data: Array<object>, value: string): Array<any> => {
found = true;
}
});

return found;
};

const result = data.filter((item) => {
return find(item, regex);
});

return result;
return data.filter((item) => find(item, regex, matchFields));
};

0 comments on commit 137d9ae

Please sign in to comment.