@hitgrab/finder 0.0.26-alpha → 0.1.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +7 -1106
  2. package/dist/core/effect-book/effect-book.d.ts +8 -0
  3. package/dist/core/events/event-emitter.d.ts +2 -1
  4. package/dist/core/filters/filters-interface.d.ts +13 -14
  5. package/dist/core/filters/filters.d.ts +15 -16
  6. package/dist/core/finder-core.d.ts +29 -51
  7. package/dist/core/group-by/group-by-interface.d.ts +7 -7
  8. package/dist/core/group-by/group-by.d.ts +12 -10
  9. package/dist/core/pagination/pagination.d.ts +5 -4
  10. package/dist/core/rule-book/rule-book.d.ts +14 -0
  11. package/dist/core/search/algorithms/sequential-characters.d.ts +1 -2
  12. package/dist/core/search/algorithms/sequential-string.d.ts +1 -2
  13. package/dist/core/search/{haystack.d.ts → result-segments/result-segment-haystack.d.ts} +1 -1
  14. package/dist/core/search/result-segments/result-segment-types.d.ts +2 -3
  15. package/dist/core/search/result-segments/search-result-segments.d.ts +3 -11
  16. package/dist/core/search/search-interface.d.ts +3 -2
  17. package/dist/core/search/search-score.d.ts +4 -0
  18. package/dist/core/search/search-string-transform.d.ts +8 -0
  19. package/dist/core/search/search.d.ts +7 -5
  20. package/dist/core/sort-by/sort-by-interface.d.ts +5 -5
  21. package/dist/core/sort-by/sort-by.d.ts +6 -5
  22. package/dist/core/tester/tester.d.ts +8 -0
  23. package/dist/core/types/core-types.d.ts +92 -0
  24. package/dist/core/types/effect-types.d.ts +22 -0
  25. package/dist/core/types/event-types.d.ts +43 -0
  26. package/dist/core/types/rule-types.d.ts +108 -0
  27. package/dist/core/utils/rule-type-enforcers.d.ts +9 -5
  28. package/dist/core/utils/rule-utils.d.ts +5 -8
  29. package/dist/index.d.ts +6 -14
  30. package/dist/index.js +3204 -3547
  31. package/dist/index.umd.cjs +30 -30
  32. package/dist/react/components/finder-content-empty.d.ts +6 -0
  33. package/dist/react/components/finder-content-groups.d.ts +7 -0
  34. package/dist/react/components/finder-content-items.d.ts +7 -0
  35. package/dist/react/components/finder-content-loading.d.ts +6 -0
  36. package/dist/react/components/finder-content-no-matches.d.ts +6 -0
  37. package/dist/react/components/finder-content.d.ts +20 -8
  38. package/dist/react/components/finder-search-term.d.ts +2 -1
  39. package/dist/react/components/finder.d.ts +7 -1
  40. package/dist/react/hooks/use-finder-constructor.d.ts +7 -0
  41. package/dist/react/hooks/use-finder-ref.d.ts +1 -1
  42. package/dist/react/hooks/use-finder.d.ts +1 -5
  43. package/dist/react/providers/finder-core-context.d.ts +3 -0
  44. package/dist/react/types/react-types.d.ts +9 -13
  45. package/package.json +4 -1
  46. package/dist/core/__tests__/selected-items.test.d.ts +0 -1
  47. package/dist/core/layout/layout-interface.d.ts +0 -19
  48. package/dist/core/layout/layout.d.ts +0 -21
  49. package/dist/core/meta/meta-interface.d.ts +0 -18
  50. package/dist/core/meta/meta.d.ts +0 -16
  51. package/dist/core/plugins/plugin-mediator.d.ts +0 -14
  52. package/dist/core/plugins/plugin-super-class.d.ts +0 -8
  53. package/dist/core/search/algorithms/unordered-characters.d.ts +0 -2
  54. package/dist/core/selected-items/selected-items-interface.d.ts +0 -22
  55. package/dist/core/selected-items/selected-items.d.ts +0 -23
  56. package/dist/core/types/internal-types.d.ts +0 -15
  57. package/dist/core/utils/string-compare-utils.d.ts +0 -13
  58. package/dist/react/components/finder-empty.d.ts +0 -6
  59. package/dist/react/components/finder-groups.d.ts +0 -7
  60. package/dist/react/components/finder-items.d.ts +0 -7
  61. package/dist/react/components/finder-loading.d.ts +0 -6
  62. package/dist/react/components/finder-no-matches.d.ts +0 -6
  63. package/dist/react/hooks/use-finder-context.d.ts +0 -2
  64. package/dist/react/providers/finder-context.d.ts +0 -3
  65. package/dist/types.d.ts +0 -221
  66. /package/dist/core/__tests__/{layout.test.d.ts → effects.test.d.ts} +0 -0
  67. /package/dist/core/__tests__/{plugins.test.d.ts → events.test.d.ts} +0 -0
package/README.md CHANGED
@@ -1,1112 +1,13 @@
1
- <a name="readme-top"></a>
1
+ # finder
2
2
 
3
- <h1 align='center'>finder</h1>
3
+ ## Headless datatable management for things that aren't tables.
4
4
 
5
- <div align='center'><small><i>Headless datatable management for things that aren't tables.</i></small></div>
5
+ Use static rules to effortlessly sort, filter, search, and group data.
6
6
 
7
- Finder provides a headless interface for manipulating datasets with easily configured rules.
7
+ # Installation
8
8
 
9
- <h4>Features</h4>
10
- <div align="center">
9
+ `npm i @hitgrab/finder`
11
10
 
12
- [![Search][Search]](#search)
13
- [![Filter][Filter]](#filters)
14
- [![sortBy][sortBy]](#sortby)
15
- [![groupBy][groupBy]](#groupby)
16
- [![Pagination][Pagination]](#pagination)
17
- [![onChangeonInit][onChangeonInit]](#life-cycle-events)
18
- [![Metadata][Metadata]](#meta)
11
+ # Documentation
19
12
 
20
- </div>
21
- <details>
22
- <summary>Table of Contents</summary>
23
- <ul>
24
- <li><a href="#basic-usage">Basic Usage</a></li>
25
- <li><a href="#search">Search</a></li>
26
- <li><a href="#filters">Filter</a></li>
27
- <li><a href="#sortby">Sort</a></li>
28
- <li><a href="#groupby">Group</a></li>
29
- <li><a href="#events">Events</a></li>
30
- <li><a href="#pagination">Pagination</a></li>
31
- <li><a href="#components-and-interfaces">Components and Interfaces</a></li>
32
- <li><a href="#meta">Meta Mixin</a></li>
33
- <li><a href="#layout">Layout Mixin</a></li>
34
- <li><a href="#hooks">Custom Hooks</a></li>
35
- <li><a href="#utils">Utils</a></li>
36
- </ul>
37
- </details>
38
-
39
- [Search]: https://img.shields.io/badge/Search-20232A?style=for-the-badge
40
- [Filter]: https://img.shields.io/badge/Filter-ffffff?style=for-the-badge
41
- [sortBy]: https://img.shields.io/badge/sort-333333?style=for-the-badge
42
- [groupBy]: https://img.shields.io/badge/group-222222?style=for-the-badge
43
- [StickyResults]: https://img.shields.io/badge/Sticky_Results-555555?style=for-the-badge
44
- [onChangeonInit]: https://img.shields.io/badge/Events-444444?style=for-the-badge
45
- [Pagination]: https://img.shields.io/badge/Paginate-888888?style=for-the-badge
46
- [Metadata]: https://img.shields.io/badge/Metadata-888888?style=for-the-badge
47
-
48
- ## Basic Usage
49
-
50
- Finder accepts an array of items, and processes them according to static rules. The matches and current rule state are passed to React context for easy consumption. A robust imperative API is provided to allow developers to build their control components in the theming library of their choice.
51
-
52
- ### Tutorial examples:
53
-
54
- 1. [Kicking Rad Shoe Store](https://stackblitz.com/edit/vitejs-vite-vsu7v2pg?file=src%2Fapp.tsx)
55
- Demonstrates filtering, sorting, and displays active rules as toggleable chips.
56
- 2. [Ancient Armory](https://stackblitz.com/edit/vitejs-vite-x4ng7k3m?file=src%2Fapp.tsx) Demonstrates searching, filtering, grouping, and the event life cycle.
57
-
58
- ### To all things a pattern
59
-
60
- Finder processes rules in the following order:
61
-
62
- 1. [Search](#search)
63
- 2. [Filters](#filters)
64
- 3. [SortBy](#sortby)
65
- 4. [Paginate](#pagination)
66
- 5. [GroupBy](#groupby)
67
-
68
- #### Basic Search Example
69
-
70
- ```ts
71
- function SearchControlComponent() {
72
- const finder = useFinderContext();
73
- return <input type="text" onInput={(e) => finder.search.setSearchTerm(e.currentTarget.value)} />
74
- }
75
-
76
- function Gallery() {
77
- const items = [...]
78
- const rules = [
79
- searchRule({
80
- searchFn: (item, searchTerm) => item.name.includes(searchTerm)
81
- })
82
- ];
83
- return(
84
- <Finder items={items} rules={rules}>
85
- <SearchControlComponent />
86
- <FinderItems>
87
- {(items) =>
88
- <div>
89
- {items.map((item) => <ItemDisplay item={item} />)}
90
- </div>
91
- </FinderItems>
92
- </Finder>
93
- )
94
- }
95
- ```
96
-
97
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
98
-
99
- ## Rules
100
-
101
- All rules ( with the exception of SearchRules ) must have a unique id for their namespace ( filters, groupBy, etc. ). Finder will throw an error if a rule is missing an id, or two filter rules have the same id.
102
-
103
- ### Search
104
-
105
- Match text against your item properties.
106
-
107
- Only a single search rule can be defined per Finder instance. If you need to do multiple kinds of text searches, consider a Filter!
108
-
109
- ```ts
110
- searchRule({
111
- searchFn: (item: FItem, searchTerm: string, meta?: FinderMeta) => boolean;
112
- debounceMilliseconds?: number;
113
- label?: string;
114
- hidden?: boolean;
115
- })
116
- ```
117
-
118
- Pro-tips:
119
-
120
- - You can use `searchRule()` to ensure your rule has a valid shape.
121
- - Use `finderStringCompare` to do a case-insensitive search that removes whitespace and line breaks.
122
- - If you have a large volume of data to process, you can add a `debounceMilliseconds`.
123
-
124
- <!--
125
- Buttons for linking to implimentation in the example repo and the docs, we can add these later when we're finalized with the structure!
126
- [![source][search-source]](/src/classes/mixins/search.ts)
127
- [![implementation][search-implementation]](https://github.com/HitGrab/finder/blob/7af28570f85b946e173072ebf4e3dcaf706ec02b/examples/react/src/app.tsx#L26)
128
-
129
- [search-source]: https://img.shields.io/badge/Source_Code-555555?style=for-the-badge
130
- [search-implementation]: https://img.shields.io/badge/example_implementation-555555?style=for-the-badge
131
- -->
132
-
133
- ### Filters
134
-
135
- Define a filter predicate that will return a boolean for each item. If multiple filters are active, _all_ filters must match for an item to be returned.
136
-
137
- ```ts
138
- filterRule({
139
- id: string;
140
- filterFn: (item: FItem, value: FValue, meta?: FinderMeta) => boolean;
141
- options?: FilterOption<FValue>[] | ((items: FItem[], meta?: FinderMeta) => FilterOption<FValue>[]);
142
- multiple?: boolean;
143
- required?: boolean;
144
- isBoolean?: boolean;
145
- defaultValue?: FValue;
146
- label?: string;
147
- hidden?: boolean;
148
- debounceMilliseconds?: number;
149
- })
150
- ```
151
-
152
- | Prop | Description | Default | Required |
153
- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
154
- | id | Every filter rule must have a unique string id. | | ✓ |
155
- | filterFn | A predicate that returns a boolean. Note that it receives the Meta mixin, which can contain instance-wide arbitrary data. | | ✓ |
156
- | options | Either an array of form options `[{label: 'Thing', value: 'thing'}]`, or an option generator function that returns options. `(items, meta) => [{label: 'Thing', value: 'thing'}]`. | | |
157
- | multiple | If this filter has a single value or an array of values. | false | |
158
- | required | Whether this filter must always have a value. If the rule provides options, the first option will be selected by default. | false | |
159
- | isBoolean | If this filter has a true/false value. Useful for checkboxes! | false | |
160
- | defaultValue | If the filter has a preset value. | | |
161
- | label | Optional label for your client to display. | | |
162
- | hidden | Optional display value for your client to display. | false | |
163
- | debounceMilliseconds | If you want to debounce value changes, enter a time in milliseconds. | | |
164
-
165
- Pro-tips:
166
-
167
- - If a rule uses an option generator function, Finder will hydrate the rule with a stable options array before emitting it to context.
168
-
169
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
170
-
171
- ### SortBy
172
-
173
- This option sorts the items passed in and accepts either a simple value or a function to run on each item to determine the order of return.
174
-
175
- ```ts
176
- sortByRule({
177
- id: string;
178
- sortFn: FinderPropertySelector<FItem> | FinderPropertySelector<FItem>[];
179
- defaultSortDirection?: SortDirection;
180
- label?: string;
181
- hidden?: boolean;
182
- })
183
- ```
184
-
185
- | Prop | Description | Default | Required |
186
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
187
- | id | Every sortBy rule must have a unique string id. | | ✓ |
188
- | sortFn | A predicate that returns a boolean. Note that it receives the Meta mixin, which can contain instance-wide arbitrary data. | | ✓ |
189
- | defaultSortDirection | 'asc' or 'desc'. | 'asc' | |
190
- | label | Optional label for your client to display. | | |
191
- | hidden | Optional display value for your client to display. | false | |
192
-
193
- Pro-tips:
194
-
195
- - Only a single sortBy rule can be active at one time.
196
- - If any sortBy rules are provided and no specific rule is set, the first sortBy rule in the stack will be considered active.
197
-
198
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
199
-
200
- ### GroupBy
201
-
202
- Takes the returned items and groups them by the passed in paramaters.
203
-
204
- ```ts
205
- groupByRule({
206
- id: string;
207
- groupFn: (item) => string | number;
208
- sortGroupIdFn?: (group) => string | number;
209
- groupIdSortDirection?: SortDirection;
210
- sticky?: {
211
- header?: string | string[];
212
- footer?: string | string[];
213
- };
214
- label?: string;
215
- hidden?: boolean;
216
- })
217
- ```
218
-
219
- | Prop | Description | Default | Required |
220
- | ------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
221
- | id | Every groupBy rule must have a unique string id. | | ✓ |
222
- | groupFn | A predicate operating on the item object that returns a string or number. Items with the same value will be grouped together. | | ✓ |
223
- | sortGroupIdFn | While any sortBy rules will operate on the items first, sortGroupIdFn allows you to sort the groups themselves. | | |
224
- | sticky | Specify group ids that should be stickied to the top or bottom of the results. | | |
225
- | label | Optional label for your client to display. | | |
226
- | hidden | Optional display value for your client to display. | false | |
227
-
228
- Pro-tips:
229
-
230
- - Only a single groupBy rule can be active at one time.
231
- - If the Finder instance has `requireGroup` enabled and no groupBy rule is active, the first rule in the stack will be considered active.
232
- - If you want a group of premium items to always appear first in the results, you can set `sticky: {header: 'premium_id' }`. If an array is provided, the order will be preserved ( i.e: `sticky: { header: ['premium_id', 'subpremium_id']})`.
233
-
234
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
235
-
236
- ## Meta Mixin
237
-
238
- The Meta mixin allows you to provide additional context that doesn't belong to either items or rules. For example, a filter might be affected by a user's purchase history or browser preferences.
239
-
240
- ```ts
241
- function MetaComponent() {
242
- const items = [...];
243
-
244
- const rules = [
245
- filterRule({
246
- id: 'user_specific_filter'
247
- filterFn: (items, value, meta) => {
248
- if (meta.get('user') === CoolUser) {
249
- return true;
250
- }
251
- return false;
252
- })
253
- ];
254
-
255
- const initialMeta = {'User': CoolUser}
256
-
257
- return (
258
- <Finder items={items} rules={rules} initialMeta={initialMeta}>
259
- // contents
260
- </Finder>
261
- )
262
- }
263
- ```
264
-
265
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
266
-
267
- ## Layout Mixin
268
-
269
- The Layout mixin can be used to inform rendering logic inside your React components.
270
-
271
- ```ts
272
- function MetaComponent() {
273
- const items = [...];
274
- const rules = [...];
275
- const layoutVariants: LayoutVariant[] = [
276
- {
277
- id: 'gallery'
278
- }
279
- {
280
- id: 'table'
281
- }
282
- ]
283
-
284
- return (
285
- <Finder items={items} rules={rules} layoutVariants={layoutVariants} initialLayout='table'>
286
- <FinderItems>
287
- {
288
- ({items, layout}) => {
289
- return layout.is('table') ? <TableComponent items={items} /> : <GalleryComponent items={items} />
290
- }
291
- }
292
- </FinderItems>
293
- </Finder>
294
- )
295
- }
296
- ```
297
-
298
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
299
-
300
- ## Pagination
301
-
302
- _Please note that pagination is not the final option considered in the functionality of the filtering / sorting process, therefore groupBy options could return unexpected results._
303
-
304
- If the `numItemsPerPage` property is set, Finder will paginate items and groups.
305
-
306
- ```ts
307
- function DeclarativePaginationComponent() {
308
- const items = [...]
309
- const rules = [...]
310
-
311
- const page = 1;
312
- const numItemsPerPage = 10;
313
-
314
- return (
315
- <Finder items={items} rules={rules} page={page} numItemsPerPage={numItemsPerPage}>
316
- ...contents
317
- </Finder>
318
- );
319
- }
320
-
321
- // the values can also be changed after Finder is initialized.
322
- function ImperativePaginationControl() {
323
- const finder = useFinderContext();
324
-
325
- return (
326
- <>
327
- // pager bar
328
- {finder.pagination.isPaginated && range(1, finder.pagination.lastPage).map((index) => {
329
- <button type="button" oncClick={() => finder.pagination.setPage(index)}>
330
- {index}
331
- </button>
332
- })}
333
-
334
- Results per page:
335
- <select onChange((e) => finder.pagination.setNumItemsPerPage(Number(e.target.value))) >
336
- <option value="10">10</option>
337
- <option value="100">100</option>
338
- <option value="1000">1000</option>
339
- </select>
340
- </>
341
- );
342
- }
343
- ```
344
-
345
- </details>
346
-
347
- ## Life Cycle Events
348
-
349
- Finder exposes an event emitter.
350
-
351
- Declarative events:
352
-
353
- ```ts
354
- <Finder items={[...items]} rules={[...]} onChange={(e) => myChangeListener(e)} />
355
- ```
356
-
357
- Imperative events:
358
-
359
- ```ts
360
- const finder = useFinderContext();
361
- finder.events.on(eventName, listener);
362
- ```
363
-
364
- ### onInit
365
-
366
- Triggered a single time when Finder is first initialized.
367
-
368
- ```ts
369
- // FinderInitEvent
370
- {
371
- source: "core";
372
- event: "init";
373
- snapshot: FinderSnapshot,
374
- timestamp: number;
375
- }
376
- ```
377
-
378
- ### onFirstUserInteraction
379
-
380
- Triggered once when the user interacts with any rule.
381
-
382
- ```ts
383
- // FinderFirstUserInteractionEvent
384
- {
385
- source: "core";
386
- event: "firstUserInteraction";
387
- snapshot: FinderSnapshot,
388
- timestamp: number;
389
- }
390
- ```
391
-
392
- ### onReady
393
-
394
- Triggered once after an items array is set and `isLoading` is false.
395
-
396
- ```ts
397
- // FinderReadyEvent
398
- {
399
- source: "core";
400
- event: "ready";
401
- snapshot: FinderSnapshot,
402
- timestamp: number;
403
- }
404
- ```
405
-
406
- ### onChange
407
-
408
- Triggered whenever a rule's state changes.
409
-
410
- ```ts
411
- // FinderChangeEvent
412
- {
413
- source: "core" | "filters" | "groupBy" | "meta" | "pagination" | "plugin" | "search" | "selectedItems" | "sortBy" | "layout";
414
- event: "change";
415
- current: any;
416
- initial: any;
417
- snapshot: FinderSnapshot,
418
- timestamp: number;
419
- }
420
- ```
421
-
422
- Pro-tips:
423
-
424
- - For change events, you can subscribe to the broad 'change' event, or a narrower event like `change.search` or `change.search.setSearchTerm`.
425
-
426
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
427
-
428
- ## Components
429
-
430
- ### Core Components
431
-
432
- <details open>
433
- <summary><i>&lt;Finder /&gt;</i> The root query container</summary>
434
- Props:
435
-
436
- ```ts
437
- items: FItem[];
438
- rules?: FinderRule<FItem>[];
439
- initialSearchTerm?: string;
440
- initialSortBy?: string;
441
- initialSortDirection?: SortDirection;
442
- initialGroupBy?: string;
443
- initialFilters?: Record<string, any>;
444
- initialMeta?: Record<string, any>;
445
- initialSelectedItems?: FItem[];
446
- maxSelectedItems?: number;
447
- isLoading?: boolean;
448
- disabled?: boolean;
449
- page?: number;
450
- numItemsPerPage?: number;
451
- requireGroup?: boolean;
452
- layoutVariants?: LayoutVariant[];
453
- initialLayout?: string;
454
- plugins?: FinderPlugin[];
455
- onInit?: (event: FinderInitEvent) => void;
456
- onReady?: (event: FinderReadyEvent) => void
457
- onFirstUserInteraction?: (event: FinderFirstUserInteractionEvent) => void;
458
- onChange?: (diff: FinderDiff, ref: FinderInstance<FItem>) => void
459
- state: 'loading' | 'empty' | 'groups' | 'items' | 'noMatches';
460
- ```
461
-
462
- </details>
463
-
464
- <details>
465
- <summary><i>&lt;FinderContent /&gt;</i> A convenience component to handle Finder states.</summary>
466
-
467
- It accepts an array of renderProps and will determine the most appropriate state to display. Only a single state can be active at a time.
468
- ```ts
469
- <FinderContent>
470
- {{
471
- // Displayed while Finder's isLoading property is true.
472
- loading: ReactNode,
473
-
474
- // Finder received an empty items array.
475
- empty: ReactNode,
476
-
477
- // No items were found that matched the current rules.
478
- noMatches: ReactNode
479
-
480
- // Items were found that matched the rules.
481
- items: ({items: FItem[], meta: MetaMixin, pagination: FinderPagination, selectedItems: FinderSelectedItems, layout: LayoutMixin}) => ReactNode,
482
-
483
- // An active GroupBy rule grouped items together.
484
- groups: ({groups: FinderResultGroup<FItem>, meta: MetaMixin, pagination: FinderPagination, selectedItems: FinderSelectedItems, layout: LayoutMixin}) => ReactNode,
485
-
486
- }}
487
-
488
- </FinderContent>
489
- ```
490
- Pro-tips:
491
-
492
- - If pagination is enabled, the items and groups components will receive only the current page's slice.
493
- </details>
494
-
495
- <details>
496
- <summary><i>&lt;FinderLoading /&gt;</i></summary>
497
-
498
- Only visible when `isLoading` is true.
499
-
500
- ```ts
501
- <FinderLoading>
502
- Requesting data: [██████__________]
503
- </FinderLoading>
504
- ```
505
-
506
- </details>
507
-
508
- <details>
509
- <summary><i>&lt;FinderEmpty /&gt;</i></summary>
510
-
511
- Only visible when `isLoading` is false, and the `items` array is empty.
512
-
513
- ```ts
514
- <FinderEmpty>
515
- Nothing here!
516
- </FinderEmpty>
517
- ```
518
-
519
- </details>
520
-
521
- <details>
522
- <summary><i>&lt;FinderNoMatches /&gt;</i></summary>
523
-
524
- Only visible when no items were found that matched the current rules.
525
-
526
- ```ts
527
- <FinderNoMatches>
528
- No items were found for that search.
529
- </FinderNoMatches>
530
- ```
531
-
532
- </details>
533
-
534
- <details>
535
- <summary><i>&lt;FinderItems /&gt;</i></summary>
536
-
537
- Only visible when items were found that matched the rules.
538
-
539
- ```ts
540
- <FinderItems>
541
- {({items: FItem[], meta: MetaMixin, pagination: FinderPagination, selectedItems: FinderSelectedItems, layout: LayoutMixin }) => ReactNode}
542
- </FinderItems>
543
- ```
544
-
545
- </details>
546
-
547
- <details>
548
- <summary><i>&lt;FinderGroups /&gt;</i></summary>
549
-
550
- Only visible when an active GroupBy rule grouped items together.
551
-
552
- ```ts
553
- <FinderItems>
554
- {({groups: FinderResultGroup<FItem>[], meta: MetaMixin, pagination: FinderPagination, selectedItems: FinderSelectedItems, layout: LayoutMixin}) => ReactNode}
555
- </FinderItems>
556
- ```
557
-
558
- </details>
559
-
560
- ### Matches
561
-
562
- Finder match results are snapshotted, and are recalculated when an internal onChange event is triggered.
563
-
564
- As a reminder, Finder processes rules in the following order:
565
-
566
- 1. Search
567
- 2. Filters
568
- 3. SortBy
569
- 4. Paginate
570
- 5. GroupBy
571
-
572
- This means that a group might be split across multiple pages as pagination is not the last option considered.
573
-
574
- <details>
575
- <summary><i>&lt;MatchesComponent /&gt;</i></summary>
576
-
577
- ```ts
578
- function MatchesComponent() {
579
- const finder = useFinderContext();
580
-
581
- return (
582
- <>
583
-
584
- // if no groupBy rule is set, the `finder.matches.items` property will be an array.
585
- {finder.matches.items?.map((item) => <Photo item={item} />)}
586
-
587
- // if a groupBy rule IS set, the `finder.matches.groups` property will be an array of result groups.
588
- {finder.matches.groups?.map((group) => (
589
- <>
590
- {group.id}
591
- {group.items.map((item) => {
592
- return <Photo item={item} />
593
- })}
594
- </>)
595
- }
596
-
597
- // total items that Finder iterated through.
598
- {finder.matches.numTotalItems}
599
- </>
600
- );
601
-
602
- }
603
-
604
- ````
605
- </details>
606
-
607
- <details>
608
- <summary><i>&lt;SearchComponent /&gt;</i></summary>
609
-
610
- ```ts
611
- function SearchComponent() {
612
- const finder = useFinderContext();
613
- const handleInput = (e) => finder.search.setSearchTerm(e.currentTarget.value);
614
- return (
615
- <>
616
- {finder.search.hasSearchTerm && `Searching for: "${finder.search.searchTerm}"`}
617
- <input type="text" onInput={handleInput} />
618
- <button type="button" onClick={() => finder.search.reset()}>
619
- Clear
620
- </button>
621
- </>
622
- );
623
- }
624
- ```
625
-
626
- </details>
627
-
628
- <details>
629
- <summary><i>&lt;FilterComponent /&gt;</i></summary>
630
-
631
- ```ts
632
- function FilterComponent({rule} : {rule: HydratedFilterRule}}) {
633
- const finder = useFinderContext();
634
-
635
- const luckyOption = rule.options.find(({value}) => value === 'lucky');
636
-
637
- // You can test the results of a filter with a specific value without applying it.
638
- const numMatches = finder.filters.test(rule, 'purple').length;
639
-
640
- // You can also test the results of *all* options for a filter with defined options. This returns a Map keyed by the Option object.
641
- const numOptionMatches = finder.filters.testOptions(rule);
642
-
643
- return (
644
- <>
645
-
646
- // retrieve the filter value
647
- {finder.filters.has(rule) && `The current value is ${finder.filters.get(rule)}`}
648
-
649
- // Check if a specific filter option is active
650
- {finder.filters.has(rule, luckyOption) && 'Super lucky!'}
651
-
652
- // filters with isBoolean can be toggled
653
- <input type="checkbox" onChange={() => finder.filters.toggle(rule)}>
654
-
655
- <select>
656
- // a blank string is considered to be setting a filter value to undefined.
657
- {rule.required === false && <option value=''>None</option>}
658
-
659
- // Rules
660
- {rule.options.map((option) => {
661
- return <option value={option.value}>{option.label}</option>
662
- })}
663
- </select>
664
-
665
- // rules that accept multiple values can toggle individual options
666
- {rule.options.map((option) => {
667
- return (
668
- <label>
669
- <input type="checkbox" value={option.value} onChange(() => finder.filters.toggleOption(rule, option)>
670
- {option.label}
671
- </label>
672
- );
673
- })}
674
-
675
- // filters will accept any value.
676
- <input type="text" onInput={(e) => finder.filters.set(rule, e.currentTarget.value))} />
677
-
678
- // reset a specific filter
679
- <button type="button" onClick={() => finder.filters.delete(rule)}>
680
- Clear
681
- </button>
682
-
683
- <button type="button" onClick={() => finder.filters.reset()}>
684
- Reset all filters
685
- </button>
686
- </>
687
- );
688
- }
689
- ```
690
-
691
- </details>
692
-
693
- <details>
694
- <summary><i>&lt;SortByComponent /&gt;</i></summary>
695
-
696
- ```ts
697
- function SortByComponent() {
698
- const finder = useFinderContext();
699
-
700
- return (
701
- <>
702
- <select value={finder.sortBy.activeRuleId} onChange={(e) => finder.sortBy.set(e.currentTarget.value)}>
703
- {finder.sortBy.rules.map((rule) => {
704
- return <option value={rule.id}>{rule.label}</option>
705
- })}
706
- </select>
707
-
708
- // Toggle sort direction between asc / desc
709
- <button type="button" onClick={() => finder.sortBy.toggleSortDirection()}>
710
- Toggle
711
- </button>
712
-
713
- // Cycle sort direction through default / asc / desc
714
- <button type="button" onClick={() => finder.sortBy.cycleSortDirection()}>
715
- Cycle
716
- </button>
717
-
718
- // reset
719
- <button type="button" onClick={() => finder.filters.delete(rule)}>
720
- Clear
721
- </button>
722
-
723
- </>
724
- );
725
- }
726
-
727
- ````
728
-
729
- </details>
730
-
731
- <details>
732
- <summary><i>&lt;GroupByComponent /&gt;</i></summary>
733
-
734
- ```ts
735
- function GroupByComponent() {
736
- const finder = useFinderContext();
737
-
738
- return (
739
- <>
740
- <select value={finder.groupBy.activeRuleId} onChange={(e) => finder.groupBy.set(e.currentTarget.value)}>
741
-
742
- // a blank string is considered to be setting the groupBy value to undefined.
743
- {finder.groupBy.requireGroup === false && <option value=''>None</option>}
744
-
745
-
746
- {finder.groupBy.rules.map((rule) => {
747
- return <option value={rule.id}>{rule.label}</option>
748
- })}
749
- </select>
750
-
751
- // Set group sort direction
752
- <button type="button" onClick={() => finder.sortBy.setGroupIdSortDirection('asc')}>
753
- Asc
754
- </button>
755
- <button type="button" onClick={() => finder.sortBy.setGroupIdSortDirection('desc')}>
756
- Desc
757
- </button>
758
-
759
- // reset
760
- <button type="button" onClick={() => finder.groupBy.reset()}>
761
- Clear
762
- </button>
763
-
764
- </>
765
- );
766
-
767
- }
768
-
769
- ```
770
-
771
- </details>
772
-
773
- <details>
774
- <summary><i>&lt;MySelectedItemsComponent /&gt;</i></summary>
775
-
776
- ```ts
777
- function MySelectedItemsComponent() {
778
-
779
- const finder = useFinderContext();
780
-
781
- return <>
782
- Selected items:
783
- {finder.selectedItems.items.map((item) => {
784
- return <SelectedItem item={item} />
785
- })}
786
-
787
- {finder.matches.items.map((item) => {
788
- return (
789
- <>
790
- // select an item
791
- <button type="button" onClick={() => finder.selectedItems.select(item)}>Add</button>
792
-
793
- // remove a selected item
794
- <button type="button" onClick={() => finder.selectedItems.delete(item)}>Remove</button>
795
-
796
- // toggle a selected item
797
- <label>
798
- <input type="checkbox" checked={finder.selectedItems.isSelected(item)} onChange(() => finder.selectedItems.toggle(item)) />
799
- </label>
800
- </>
801
- );
802
- })}
803
-
804
- // remove all selected items
805
- <button type="button" onClick={() => finder.selectedItems.reset()}>Clear All</button>
806
- </>
807
- }
808
- ```
809
-
810
- </details>
811
-
812
- ## Hooks
813
-
814
- ### useFinder(items, options)
815
-
816
- Create a new Finder instance.
817
-
818
- ### useFinderContext()
819
-
820
- Consume a parent Finder context.
821
-
822
- ### useFinderRef()
823
-
824
- A convenience hook for controlling Finder from the same scope it's created in.
825
-
826
- ```ts
827
- function MyComponent() {
828
-
829
- const ref = useFinderRef();
830
- ref.current.events.on(eventName, eventListener);
831
- return <Finder controllerRef={ref}>...</Finder>
832
-
833
- }
834
- ```
835
-
836
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
837
-
838
- ## Imperative Interfaces
839
-
840
- All interfaces can be accessed from the `useFinderContext()` hook.
841
-
842
- ### Matches
843
-
844
- ```ts
845
- finder.matches = {
846
- items?: FItem[];
847
- groups?: FinderResultGroup<FItem>[];
848
- numMatchedItems: number;
849
- numTotalItems: number;
850
- hasGroupByRule: boolean;
851
- }
852
- ```
853
-
854
- ### Search
855
-
856
- ```ts
857
- finder.search = {
858
- searchTerm: string;
859
- activeRule?: SearchRule<FItem>;
860
- hasSearchTerm: boolean;
861
- setSearchTerm: (value: string) => void;
862
- reset: () => void;
863
- };
864
- ```
865
-
866
- ### Filters
867
-
868
- ```ts
869
- finder.filters = {
870
- filters: Record<string, any>; /* Formatted and type-cast values including default values, etc. */
871
- raw: Record<string, any>; /* Raw mixin state without default values or formatting */
872
- activeRules: HydratedFilterRule<FItem>[];
873
- rules: HydratedFilterRule<FItem>[];
874
- isActive: (identifier: string | FilterRule | HydratedFilterRule, value?: any) => boolean;
875
- set: (identifier: string | FilterRule | HydratedFilterRule, value?: any) => void;
876
- get: (identifier: string | FilterRule | HydratedFilterRule) => any;
877
- has: (identifier: string | FilterRule | HydratedFilterRule) => boolean;
878
- delete: (identifier: string | FilterRule | HydratedFilterRule) => void;
879
- toggle: (identifier: string | FilterRule | HydratedFilterRule) => void;
880
- toggleOption: (identifier: string | FilterRule | HydratedFilterRule, optionValue: FinderOption | any) => void;
881
- getRule: (identifier:string) => HydratedFilterRule | undefined;
882
- test: (rule: FilterTestOptions) => FItem[];
883
- testRule: (identifier: string | FilterRule | HydratedFilterRule, filterValue: any, meta?: FinderMeta) => FItem[];
884
- testOptions: (identifier: string | FilterRule | HydratedFilterRule, meta?: FinderMeta) => Map<FinderOption | boolean, FItem[] | undefined>;
885
- };
886
- ```
887
-
888
- ### SortBy
889
-
890
- ```ts
891
- finder.sortBy = {
892
- activeRule?: HydratedSortByRule<FItem>;
893
- activeRuleId?: string;
894
- rules: HydratedSortByRule<FItem>[];
895
- sortDirection?: string;
896
- set: (identifier?: string | SortByRule | HydratedSortByRule, sortDirection?: string) => void;
897
- setSortDirection: (sortDirection?: string) => void;
898
- cycleSortDirection: () => void;
899
- toggleSortDirection: () => void;
900
- };
901
- ```
902
-
903
- ### GroupBy
904
-
905
- ```ts
906
- finder.groupBy = {
907
- activeRule?: GroupByRule<FItem>;
908
- activeRuleId?: string;
909
- rules: GroupByRule<FItem>[];
910
- requireGroup: boolean;
911
- groupIdSortDirection?: string;
912
- set: (identifier?: GroupByRule | string, value?: string) => void;
913
- toggle: (identifier: GroupByRule | string) => void;
914
- setGroupIdSortDirection: (direction?: string) => void;
915
- reset: () => void;
916
- };
917
- ```
918
-
919
- ### Meta
920
-
921
- ```ts
922
- finder.meta = {
923
- value?: FinderMeta;
924
- set: (metaIdentifier: any, metaValue: any) => void;
925
- get: (metaIdentifier: any) => any;
926
- has: (metaIdentifier: any) => boolean;
927
- delete: (metaIdentifier: any) => void;
928
- reset: () => void;
929
- };
930
- ```
931
-
932
- ### Layout
933
-
934
- ```ts
935
- finder.layout = {
936
- variants?: LayoutVariant[];
937
- activeLayout?: LayoutVariant;
938
- raw?: LayoutVariant;
939
- is: (layoutIdentifier: string | LayoutVariant) => boolean;
940
- set: (layoutIdentifier: string | LayoutVariant) => void;
941
- reset: () => void;
942
- };
943
- ```
944
-
945
- ### Pagination
946
-
947
- ```ts
948
- finder.pagination = {
949
- page: number;
950
- offset: number;
951
- numItemsPerPage?: number;
952
- numTotalItems: number;
953
- lastPage?: number;
954
- isPaginated: boolean;
955
- setPage: (page: number) => void;
956
- setNumItemsPerPage: (numItemsPerPage: number) => void;
957
- };
958
- ```
959
-
960
- ### Selected Items
961
-
962
- ```ts
963
- finder.selectedItems = {
964
- items?: FItem[];
965
- select: (item: FItem) => void;
966
- delete: (item: FItem) => void;
967
- toggle: (item: FItem) => void;
968
- selectOnly: (item: FItem) => void;
969
- toggleOnly: (item: FItem) => void;
970
- isSelected: (item: FItem) => boolean;
971
- reset: () => void;
972
- };
973
- ```
974
-
975
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
976
-
977
- ## Type Guards
978
-
979
- Ensures that rules have the correct shape.
980
-
981
- ### searchRule
982
-
983
- ```ts
984
- const rule = searchRule<FItem>({
985
- searchFn: (item, searchTerm) => boolean,
986
- });
987
- ```
988
-
989
- ### filterRule
990
-
991
- ```ts
992
- const rule = filterRule<FItem, FValue>({
993
- id: "unique_identifier",
994
- filterFn: (item, value, meta) => boolean,
995
- });
996
- ```
997
-
998
- ### sortByRule()
999
-
1000
- ```ts
1001
- const rule = sortByRule<FItem>({
1002
- id: "unique_identifier",
1003
- sortFn: (item) => string | number,
1004
- });
1005
- ```
1006
-
1007
- ### groupByRule
1008
-
1009
- ```ts
1010
- const rule = groupByRule<FItem>({
1011
- id: "unique_identifier",
1012
- groupFn: (item) => string | number,
1013
- });
1014
- ```
1015
-
1016
- ### finderRuleset
1017
-
1018
- Ensures that an array of rules all have the correct shape.
1019
-
1020
- ```ts
1021
- const rule = finderRules<FItem>([
1022
- {
1023
- id: "unique_identifier",
1024
- sortFn: (item) => string | number,
1025
- },
1026
- {
1027
- id: "unique_identifier",
1028
- groupFn: (item) => string | number,
1029
- },
1030
- ]);
1031
- ```
1032
-
1033
- ## String Comparison Utils
1034
-
1035
- ### finderStringCompare
1036
-
1037
- ```ts
1038
- finderStringCompare(haystack: string, needle: string, aliases?: string[])
1039
- ```
1040
-
1041
- Performs a case-insensitive search that removes whitespace and line breaks. If an `aliases` array is provided, the needle will be compared against all alias entries as well.
1042
-
1043
- ```ts
1044
- // with mangled search term with line breaks and too much white space
1045
- finderStringCompare("guava", "g u a \n v \r"); // true
1046
-
1047
- // with aliases
1048
- finderStringCompare("guava", "gu", ["guajava", "guayaba"]); // true
1049
- ```
1050
-
1051
- ### finderCharacterCompare
1052
-
1053
- ```ts
1054
- finderCharacterCompare(haystack: string, needle: string, aliases?: string[])
1055
- ```
1056
-
1057
- Performs a case-insensitive comparison of needle characters against the haystack. The characters can appear in any order.
1058
-
1059
- ```ts
1060
- finderCharacterCompare("e d c b a", "AB C\nD\r E"); // true
1061
- ```
1062
-
1063
- ### finderCharacterCompare
1064
-
1065
- ```ts
1066
- finderCharacterCompare(haystack: string, needle: string, aliases?: string[])
1067
- ```
1068
-
1069
- Performs a case-insensitive comparison of needle characters against the haystack. The characters can appear in any order.
1070
-
1071
- ```ts
1072
- finderSequentialCharacterCompare("aabciop[cde", "AB C\nD\r E"); // true
1073
-
1074
- finderSequentialCharacterCompare("e d c b a", "AB C\nD\r E"); // false
1075
- ```
1076
-
1077
- Performs a case-insensitive comparison of needle characters against the haystack. The characters must appear in the same order as the needle.
1078
-
1079
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
1080
-
1081
- ## Plugins
1082
-
1083
- ```ts
1084
- const plugin = FinderPluginInterface<FItem> {
1085
- id: string;
1086
- register: (finder: FinderCore<FItem>, touch: FinderTouchCallback) => void;
1087
- }
1088
- ```
1089
-
1090
- For power users, Finder accepts plugins that can attach event listeners and trigger internal actions.
1091
-
1092
- ```ts
1093
- // example
1094
- const plugin = FinderPluginInterface<FItem> {
1095
- id: 'my_cool_plugin';
1096
- register: (finder: FinderCore<FItem>, touch: FinderTouchCallback) => {
1097
- finder.events.on('change.filters.set', (event) => {
1098
- // modify something
1099
- ...
1100
-
1101
- // trigger a state update in Finder
1102
- touch();
1103
- })
1104
- };
1105
- }
1106
-
1107
- return <Finder items={[...]} plugins={[plugin]}>...</Finder>
1108
- ```
1109
-
1110
- A convenience `FinderPlugin` class is exported for users who prefer an object-oriented approach.
1111
-
1112
- <p align="right">(<a href="#readme-top">back to top</a>)</p>
13
+ Find complete documentation at https://hitgrab.github.io/finder/