native-document 1.0.91 → 1.0.93
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.
- package/dist/native-document.components.min.js +1168 -138
- package/dist/native-document.dev.js +792 -217
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.devtools.min.js +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/advanced-components.md +814 -0
- package/docs/anchor.md +71 -11
- package/docs/cache.md +888 -0
- package/docs/conditional-rendering.md +91 -1
- package/docs/core-concepts.md +9 -2
- package/docs/elements.md +127 -2
- package/docs/extending-native-document-element.md +7 -1
- package/docs/filters.md +1216 -0
- package/docs/getting-started.md +12 -3
- package/docs/lifecycle-events.md +10 -2
- package/docs/list-rendering.md +453 -54
- package/docs/memory-management.md +9 -7
- package/docs/native-document-element.md +30 -9
- package/docs/native-fetch.md +744 -0
- package/docs/observables.md +135 -6
- package/docs/routing.md +7 -1
- package/docs/state-management.md +7 -1
- package/docs/validation.md +8 -1
- package/eslint.config.js +3 -3
- package/package.json +3 -2
- package/readme.md +53 -14
- package/src/components/$traits/HasItems.js +42 -1
- package/src/components/BaseComponent.js +4 -1
- package/src/components/accordion/Accordion.js +112 -8
- package/src/components/accordion/AccordionItem.js +93 -4
- package/src/components/alert/Alert.js +164 -4
- package/src/components/avatar/Avatar.js +236 -22
- package/src/components/menu/index.js +1 -2
- package/src/core/data/ObservableArray.js +120 -2
- package/src/core/data/ObservableChecker.js +50 -0
- package/src/core/data/ObservableItem.js +223 -80
- package/src/core/data/ObservableWhen.js +36 -6
- package/src/core/data/observable-helpers/array.js +12 -3
- package/src/core/data/observable-helpers/computed.js +17 -4
- package/src/core/data/observable-helpers/object.js +19 -3
- package/src/core/elements/control/for-each-array.js +21 -3
- package/src/core/elements/control/for-each.js +17 -5
- package/src/core/elements/control/show-if.js +31 -15
- package/src/core/elements/control/show-when.js +23 -0
- package/src/core/elements/control/switch.js +40 -10
- package/src/core/utils/cache.js +5 -0
- package/src/core/utils/memoize.js +25 -16
- package/src/core/utils/prototypes.js +3 -2
- package/src/core/wrappers/AttributesWrapper.js +1 -1
- package/src/core/wrappers/NDElement.js +41 -1
- package/src/core/wrappers/NdPrototype.js +4 -0
- package/src/core/wrappers/TemplateCloner.js +13 -10
- package/src/core/wrappers/prototypes/bind-class-extensions.js +1 -1
- package/src/core/wrappers/prototypes/nd-element-extensions.js +3 -0
- package/src/router/Route.js +9 -4
- package/src/router/Router.js +28 -9
- package/src/router/errors/RouterError.js +0 -1
- package/types/control-flow.d.ts +9 -6
- package/types/elements.d.ts +6 -3
- package/types/filters/index.d.ts +4 -0
- package/types/nd-element.d.ts +5 -238
- package/types/observable.d.ts +9 -3
- package/types/router.d.ts +5 -1
- package/types/template-cloner.ts +1 -0
- package/types/validator.ts +11 -1
- package/utils.d.ts +2 -1
- package/utils.js +4 -4
- package/src/core/utils/service.js +0 -6
package/docs/filters.md
ADDED
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
# Filters
|
|
2
|
+
|
|
3
|
+
NativeDocument provides a comprehensive set of filter helpers for creating reactive, type-safe data filtering with Observable arrays. These filters work seamlessly with the `where()`, `whereSome()`, and `whereEvery()` methods.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Filter helpers enable:
|
|
8
|
+
- **Reactive filtering** - Filters update automatically when observables change
|
|
9
|
+
- **Type-safe comparisons** - Validate data types and formats
|
|
10
|
+
- **Composable logic** - Combine filters with `and`, `or`, `not`
|
|
11
|
+
- **Date/Time handling** - Specialized filters for temporal data
|
|
12
|
+
- **Custom filters** - Create your own filter logic
|
|
13
|
+
|
|
14
|
+
## Import
|
|
15
|
+
```javascript
|
|
16
|
+
import { filters } from 'native-document/utils';
|
|
17
|
+
const { equals, greaterThan, between, includes, and, or, not } = filters;
|
|
18
|
+
|
|
19
|
+
// Or destructure directly
|
|
20
|
+
import { filters: { equals, greaterThan, between, includes } } from 'native-document/utils';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Filters
|
|
24
|
+
|
|
25
|
+
### Comparison Filters
|
|
26
|
+
```javascript
|
|
27
|
+
import { filters } from 'native-document/utils';
|
|
28
|
+
const { equals, notEquals, greaterThan, lessThan } = filters;
|
|
29
|
+
import { Observable } from 'native-document';
|
|
30
|
+
|
|
31
|
+
const products = Observable.array([
|
|
32
|
+
{ id: 1, name: 'Phone', price: 599, stock: 10 },
|
|
33
|
+
{ id: 2, name: 'Laptop', price: 999, stock: 5 },
|
|
34
|
+
{ id: 3, name: 'Tablet', price: 399, stock: 0 },
|
|
35
|
+
{ id: 4, name: 'Watch', price: 299, stock: 15 }
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Filter by exact value
|
|
39
|
+
const expensive = products.where({
|
|
40
|
+
price: equals(999)
|
|
41
|
+
});
|
|
42
|
+
// Result: [{ id: 2, name: 'Laptop', price: 999, stock: 5 }]
|
|
43
|
+
|
|
44
|
+
// Filter by inequality
|
|
45
|
+
const notTablet = products.where({
|
|
46
|
+
name: notEquals('Tablet')
|
|
47
|
+
});
|
|
48
|
+
// Result: All except Tablet
|
|
49
|
+
|
|
50
|
+
// Greater than
|
|
51
|
+
const expensiveProducts = products.where({
|
|
52
|
+
price: greaterThan(500)
|
|
53
|
+
});
|
|
54
|
+
// Result: Phone and Laptop
|
|
55
|
+
|
|
56
|
+
// Less than
|
|
57
|
+
const affordable = products.where({
|
|
58
|
+
price: lessThan(400)
|
|
59
|
+
});
|
|
60
|
+
// Result: Watch and Tablet
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Shortcuts
|
|
64
|
+
```javascript
|
|
65
|
+
import { filters } from 'native-document/utils';
|
|
66
|
+
const { eq, neq, gt, gte, lt, lte } = filters;
|
|
67
|
+
|
|
68
|
+
const products = Observable.array([...]);
|
|
69
|
+
|
|
70
|
+
// Short aliases
|
|
71
|
+
const expensive = products.where({ price: gt(500) });
|
|
72
|
+
const affordable = products.where({ price: lte(400) });
|
|
73
|
+
const notPhone = products.where({ name: neq('Phone') });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### greaterThanOrEqual() / lessThanOrEqual()
|
|
77
|
+
```javascript
|
|
78
|
+
import { filters } from 'native-document/utils';
|
|
79
|
+
const { greaterThanOrEqual, lessThanOrEqual } = filters;
|
|
80
|
+
import { Observable } from 'native-document';
|
|
81
|
+
|
|
82
|
+
const products = Observable.array([
|
|
83
|
+
{ name: 'Budget', price: 100 },
|
|
84
|
+
{ name: 'Standard', price: 500 },
|
|
85
|
+
{ name: 'Premium', price: 1000 }
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Greater than or equal to
|
|
89
|
+
const standardOrBetter = products.where({
|
|
90
|
+
price: greaterThanOrEqual(500)
|
|
91
|
+
});
|
|
92
|
+
// Result: Standard, Premium
|
|
93
|
+
|
|
94
|
+
// Less than or equal to
|
|
95
|
+
const budgetFriendly = products.where({
|
|
96
|
+
price: lessThanOrEqual(500)
|
|
97
|
+
});
|
|
98
|
+
// Result: Budget, Standard
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Range Filters
|
|
102
|
+
|
|
103
|
+
### between()
|
|
104
|
+
```javascript
|
|
105
|
+
import { filters } from 'native-document/utils';
|
|
106
|
+
const { between } = filters;
|
|
107
|
+
import { Observable } from 'native-document';
|
|
108
|
+
|
|
109
|
+
const products = Observable.array([
|
|
110
|
+
{ name: 'Budget Phone', price: 199 },
|
|
111
|
+
{ name: 'Mid Phone', price: 499 },
|
|
112
|
+
{ name: 'Premium Phone', price: 999 },
|
|
113
|
+
{ name: 'Luxury Phone', price: 1499 }
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
// Static range
|
|
117
|
+
const midRange = products.where({
|
|
118
|
+
price: between(400, 800)
|
|
119
|
+
});
|
|
120
|
+
// Result: [{ name: 'Mid Phone', price: 499 }]
|
|
121
|
+
|
|
122
|
+
// Reactive range with observables
|
|
123
|
+
const minPrice = Observable(200);
|
|
124
|
+
const maxPrice = Observable(1000);
|
|
125
|
+
|
|
126
|
+
const filtered = products.where({
|
|
127
|
+
price: between(minPrice, maxPrice)
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Updates automatically when bounds change
|
|
131
|
+
minPrice.set(500); // Now shows only Premium Phone
|
|
132
|
+
maxPrice.set(600); // Now shows nothing
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## String Filters
|
|
136
|
+
|
|
137
|
+
### includes() / contains()
|
|
138
|
+
```javascript
|
|
139
|
+
import { filters } from 'native-document/utils';
|
|
140
|
+
const { includes, contains } = filters;
|
|
141
|
+
import { Observable } from 'native-document';
|
|
142
|
+
|
|
143
|
+
const products = Observable.array([
|
|
144
|
+
{ name: 'iPhone 15 Pro' },
|
|
145
|
+
{ name: 'Samsung Galaxy' },
|
|
146
|
+
{ name: 'Google Pixel' },
|
|
147
|
+
{ name: 'OnePlus Phone' }
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
// Case-insensitive by default
|
|
151
|
+
const phones = products.where({
|
|
152
|
+
name: includes('phone')
|
|
153
|
+
});
|
|
154
|
+
// Result: iPhone 15 Pro, OnePlus Phone
|
|
155
|
+
|
|
156
|
+
// Reactive search
|
|
157
|
+
const searchTerm = Observable('galaxy');
|
|
158
|
+
const results = products.where({
|
|
159
|
+
name: includes(searchTerm)
|
|
160
|
+
});
|
|
161
|
+
// Result: Samsung Galaxy
|
|
162
|
+
|
|
163
|
+
searchTerm.set('pixel');
|
|
164
|
+
// Result: Google Pixel
|
|
165
|
+
|
|
166
|
+
// contains is an alias
|
|
167
|
+
const sameResults = products.where({
|
|
168
|
+
name: contains('phone')
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### startsWith()
|
|
173
|
+
```javascript
|
|
174
|
+
import { filters } from 'native-document/utils';
|
|
175
|
+
const { startsWith } = filters;
|
|
176
|
+
import { Observable } from 'native-document';
|
|
177
|
+
|
|
178
|
+
const users = Observable.array([
|
|
179
|
+
{ name: 'Alice Johnson' },
|
|
180
|
+
{ name: 'Bob Smith' },
|
|
181
|
+
{ name: 'Alice Brown' },
|
|
182
|
+
{ name: 'Charlie Wilson' }
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
// Case-insensitive by default
|
|
186
|
+
const alices = users.where({
|
|
187
|
+
name: startsWith('alice')
|
|
188
|
+
});
|
|
189
|
+
// Result: Alice Johnson, Alice Brown
|
|
190
|
+
|
|
191
|
+
// Case-sensitive (second parameter)
|
|
192
|
+
const caseSensitive = users.where({
|
|
193
|
+
name: startsWith('Alice', true)
|
|
194
|
+
});
|
|
195
|
+
// Result: Alice Johnson, Alice Brown
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### endsWith()
|
|
199
|
+
```javascript
|
|
200
|
+
import { filters } from 'native-document/utils';
|
|
201
|
+
const { endsWith } = filters;
|
|
202
|
+
import { Observable } from 'native-document';
|
|
203
|
+
|
|
204
|
+
const files = Observable.array([
|
|
205
|
+
{ name: 'document.pdf' },
|
|
206
|
+
{ name: 'image.jpg' },
|
|
207
|
+
{ name: 'report.pdf' },
|
|
208
|
+
{ name: 'photo.png' }
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
// Case-insensitive by default
|
|
212
|
+
const pdfs = files.where({
|
|
213
|
+
name: endsWith('.pdf')
|
|
214
|
+
});
|
|
215
|
+
// Result: document.pdf, report.pdf
|
|
216
|
+
|
|
217
|
+
// Case-sensitive
|
|
218
|
+
const pdfsCaseSensitive = files.where({
|
|
219
|
+
name: endsWith('.PDF', true)
|
|
220
|
+
});
|
|
221
|
+
// Result: [] (none match uppercase)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### match()
|
|
225
|
+
```javascript
|
|
226
|
+
import { filters } from 'native-document/utils';
|
|
227
|
+
const { match } = filters;
|
|
228
|
+
import { Observable } from 'native-document';
|
|
229
|
+
|
|
230
|
+
const products = Observable.array([
|
|
231
|
+
{ sku: 'ABC-123' },
|
|
232
|
+
{ sku: 'DEF-456' },
|
|
233
|
+
{ sku: 'GHI-789' },
|
|
234
|
+
{ sku: 'INVALID' }
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
// Regex pattern
|
|
238
|
+
const validSKUs = products.where({
|
|
239
|
+
sku: match(/^[A-Z]{3}-\d{3}$/, true)
|
|
240
|
+
});
|
|
241
|
+
// Result: ABC-123, DEF-456, GHI-789
|
|
242
|
+
|
|
243
|
+
// Simple text match (no regex)
|
|
244
|
+
const containsABC = products.where({
|
|
245
|
+
sku: match('ABC', false)
|
|
246
|
+
});
|
|
247
|
+
// Result: ABC-123
|
|
248
|
+
|
|
249
|
+
// With flags
|
|
250
|
+
const caseInsensitive = products.where({
|
|
251
|
+
sku: match(/abc/, true, 'i')
|
|
252
|
+
});
|
|
253
|
+
// Result: ABC-123
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Array Filters
|
|
257
|
+
|
|
258
|
+
### inArray()
|
|
259
|
+
```javascript
|
|
260
|
+
import { filters } from 'native-document/utils';
|
|
261
|
+
const { inArray } = filters;
|
|
262
|
+
import { Observable } from 'native-document';
|
|
263
|
+
|
|
264
|
+
const products = Observable.array([
|
|
265
|
+
{ id: 1, category: 'electronics' },
|
|
266
|
+
{ id: 2, category: 'books' },
|
|
267
|
+
{ id: 3, category: 'clothing' },
|
|
268
|
+
{ id: 4, category: 'electronics' }
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
// Static array
|
|
272
|
+
const allowed = products.where({
|
|
273
|
+
category: inArray(['electronics', 'books'])
|
|
274
|
+
});
|
|
275
|
+
// Result: items 1, 2, 4
|
|
276
|
+
|
|
277
|
+
// Reactive array
|
|
278
|
+
const allowedCategories = Observable.array(['electronics']);
|
|
279
|
+
const filtered = products.where({
|
|
280
|
+
category: inArray(allowedCategories)
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Updates when array changes
|
|
284
|
+
allowedCategories.push('books');
|
|
285
|
+
// Now includes books too
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### notIn()
|
|
289
|
+
```javascript
|
|
290
|
+
import { filters } from 'native-document/utils';
|
|
291
|
+
const { notIn } = filters;
|
|
292
|
+
import { Observable } from 'native-document';
|
|
293
|
+
|
|
294
|
+
const users = Observable.array([
|
|
295
|
+
{ id: 1, status: 'active' },
|
|
296
|
+
{ id: 2, status: 'banned' },
|
|
297
|
+
{ id: 3, status: 'inactive' },
|
|
298
|
+
{ id: 4, status: 'active' }
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const validUsers = users.where({
|
|
302
|
+
status: notIn(['banned', 'deleted'])
|
|
303
|
+
});
|
|
304
|
+
// Result: items 1, 3, 4
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Empty/Existence Filters
|
|
308
|
+
|
|
309
|
+
### isEmpty()
|
|
310
|
+
```javascript
|
|
311
|
+
import { filters } from 'native-document/utils';
|
|
312
|
+
const { isEmpty } = filters;
|
|
313
|
+
import { Observable } from 'native-document';
|
|
314
|
+
|
|
315
|
+
const tasks = Observable.array([
|
|
316
|
+
{ title: 'Task 1', description: '' },
|
|
317
|
+
{ title: 'Task 2', description: 'Details' },
|
|
318
|
+
{ title: 'Task 3', description: null },
|
|
319
|
+
{ title: 'Task 4', tags: [] }
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
// Empty string or null
|
|
323
|
+
const noDescription = tasks.where({
|
|
324
|
+
description: isEmpty()
|
|
325
|
+
});
|
|
326
|
+
// Result: Task 1, Task 3
|
|
327
|
+
|
|
328
|
+
// Empty arrays
|
|
329
|
+
const noTags = tasks.where({
|
|
330
|
+
tags: isEmpty()
|
|
331
|
+
});
|
|
332
|
+
// Result: Task 4
|
|
333
|
+
|
|
334
|
+
// Conditional empty
|
|
335
|
+
const shouldBeEmpty = Observable(true);
|
|
336
|
+
const filtered = tasks.where({
|
|
337
|
+
description: isEmpty(shouldBeEmpty)
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
shouldBeEmpty.set(false);
|
|
341
|
+
// Now returns items with non-empty descriptions
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### isNotEmpty()
|
|
345
|
+
```javascript
|
|
346
|
+
import { filters } from 'native-document/utils';
|
|
347
|
+
const { isNotEmpty } = filters;
|
|
348
|
+
import { Observable } from 'native-document';
|
|
349
|
+
|
|
350
|
+
const tasks = Observable.array([
|
|
351
|
+
{ title: 'Task 1', description: '' },
|
|
352
|
+
{ title: 'Task 2', description: 'Details' },
|
|
353
|
+
{ title: 'Task 3', description: null }
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
const withDescription = tasks.where({
|
|
357
|
+
description: isNotEmpty()
|
|
358
|
+
});
|
|
359
|
+
// Result: Task 2
|
|
360
|
+
|
|
361
|
+
// Conditional
|
|
362
|
+
const shouldHaveContent = Observable(true);
|
|
363
|
+
const filtered = tasks.where({
|
|
364
|
+
description: isNotEmpty(shouldHaveContent)
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Date and Time Filters
|
|
369
|
+
|
|
370
|
+
All date and time filters automatically convert values to Date objects using the internal `toDate()` helper. You can pass Date objects, timestamps, or date strings - they will be converted automatically.
|
|
371
|
+
|
|
372
|
+
### Date Comparison
|
|
373
|
+
```javascript
|
|
374
|
+
import { filters } from 'native-document/utils';
|
|
375
|
+
const { dateEquals, dateBefore, dateAfter, dateBetween } = filters;
|
|
376
|
+
import { Observable } from 'native-document';
|
|
377
|
+
|
|
378
|
+
const events = Observable.array([
|
|
379
|
+
{ name: 'Meeting', date: '2024-01-15' },
|
|
380
|
+
{ name: 'Conference', date: '2024-06-20' },
|
|
381
|
+
{ name: 'Workshop', date: '2024-09-10' },
|
|
382
|
+
{ name: 'Seminar', date: '2024-12-05' }
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
// Specific date (string automatically converted to Date)
|
|
386
|
+
const januaryEvents = events.where({
|
|
387
|
+
date: dateEquals('2024-01-15')
|
|
388
|
+
});
|
|
389
|
+
// Result: Meeting
|
|
390
|
+
|
|
391
|
+
// Or with Date object
|
|
392
|
+
const januaryEvents2 = events.where({
|
|
393
|
+
date: dateEquals(new Date('2024-01-15'))
|
|
394
|
+
});
|
|
395
|
+
// Result: Meeting
|
|
396
|
+
|
|
397
|
+
// Before a date
|
|
398
|
+
const firstHalf = events.where({
|
|
399
|
+
date: dateBefore('2024-07-01')
|
|
400
|
+
});
|
|
401
|
+
// Result: Meeting, Conference
|
|
402
|
+
|
|
403
|
+
// After a date
|
|
404
|
+
const secondHalf = events.where({
|
|
405
|
+
date: dateAfter('2024-07-01')
|
|
406
|
+
});
|
|
407
|
+
// Result: Workshop, Seminar
|
|
408
|
+
|
|
409
|
+
// Date range
|
|
410
|
+
const summerEvents = events.where({
|
|
411
|
+
date: dateBetween('2024-06-01', '2024-08-31')
|
|
412
|
+
});
|
|
413
|
+
// Result: Conference
|
|
414
|
+
|
|
415
|
+
// Reactive date filtering with observables
|
|
416
|
+
const startDate = Observable('2024-01-01');
|
|
417
|
+
const endDate = Observable('2024-06-30');
|
|
418
|
+
|
|
419
|
+
const filtered = events.where({
|
|
420
|
+
date: dateBetween(startDate, endDate)
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Updates when dates change
|
|
424
|
+
endDate.set('2024-12-31');
|
|
425
|
+
// Now includes all events
|
|
426
|
+
|
|
427
|
+
// Works with timestamps too
|
|
428
|
+
const timestamp = Date.now();
|
|
429
|
+
const recentEvents = events.where({
|
|
430
|
+
date: dateAfter(timestamp)
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Time Comparison (Ignores Date)
|
|
435
|
+
|
|
436
|
+
Time filters extract and compare only the time portion (hours, minutes, seconds), ignoring the date.
|
|
437
|
+
```javascript
|
|
438
|
+
import { filters } from 'native-document/utils';
|
|
439
|
+
const { timeEquals, timeBefore, timeAfter, timeBetween } = filters;
|
|
440
|
+
import { Observable } from 'native-document';
|
|
441
|
+
|
|
442
|
+
const appointments = Observable.array([
|
|
443
|
+
{ name: 'Breakfast', time: '2024-01-15 08:00:00' },
|
|
444
|
+
{ name: 'Meeting', time: '2024-01-15 14:00:00' },
|
|
445
|
+
{ name: 'Dinner', time: '2024-01-15 19:00:00' }
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
// Specific time (date is ignored, only time matters)
|
|
449
|
+
const lunchTime = appointments.where({
|
|
450
|
+
time: timeEquals('2024-01-01 14:00:00')
|
|
451
|
+
});
|
|
452
|
+
// Result: Meeting (date doesn't need to match)
|
|
453
|
+
|
|
454
|
+
// Before a time
|
|
455
|
+
const morning = appointments.where({
|
|
456
|
+
time: timeBefore('12:00:00')
|
|
457
|
+
});
|
|
458
|
+
// Result: Breakfast
|
|
459
|
+
|
|
460
|
+
// After a time
|
|
461
|
+
const evening = appointments.where({
|
|
462
|
+
time: timeAfter('18:00:00')
|
|
463
|
+
});
|
|
464
|
+
// Result: Dinner
|
|
465
|
+
|
|
466
|
+
// Time range (9 AM to 5 PM)
|
|
467
|
+
const businessHours = appointments.where({
|
|
468
|
+
time: timeBetween('09:00:00', '17:00:00')
|
|
469
|
+
});
|
|
470
|
+
// Result: Meeting
|
|
471
|
+
|
|
472
|
+
// Works with any date - only time is compared
|
|
473
|
+
const businessHours2 = appointments.where({
|
|
474
|
+
time: timeBetween('2025-12-25 09:00:00', '2025-12-25 17:00:00')
|
|
475
|
+
});
|
|
476
|
+
// Result: Meeting (same result, date is ignored)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### DateTime Comparison (Date + Time)
|
|
480
|
+
|
|
481
|
+
DateTime filters compare both date and time together for exact timestamp matching.
|
|
482
|
+
```javascript
|
|
483
|
+
import { filters } from 'native-document/utils';
|
|
484
|
+
const { dateTimeEquals, dateTimeBefore, dateTimeAfter, dateTimeBetween } = filters;
|
|
485
|
+
import { Observable } from 'native-document';
|
|
486
|
+
|
|
487
|
+
const logs = Observable.array([
|
|
488
|
+
{ message: 'Start', timestamp: '2024-01-15 08:30:00' },
|
|
489
|
+
{ message: 'Process', timestamp: '2024-01-15 14:45:00' },
|
|
490
|
+
{ message: 'End', timestamp: '2024-01-15 18:20:00' }
|
|
491
|
+
]);
|
|
492
|
+
|
|
493
|
+
// Exact timestamp (both date and time must match)
|
|
494
|
+
const exactLog = logs.where({
|
|
495
|
+
timestamp: dateTimeEquals('2024-01-15 14:45:00')
|
|
496
|
+
});
|
|
497
|
+
// Result: Process
|
|
498
|
+
|
|
499
|
+
// Before timestamp
|
|
500
|
+
const earlyLogs = logs.where({
|
|
501
|
+
timestamp: dateTimeBefore('2024-01-15 15:00:00')
|
|
502
|
+
});
|
|
503
|
+
// Result: Start, Process
|
|
504
|
+
|
|
505
|
+
// After timestamp
|
|
506
|
+
const lateLogs = logs.where({
|
|
507
|
+
timestamp: dateTimeAfter('2024-01-15 15:00:00')
|
|
508
|
+
});
|
|
509
|
+
// Result: End
|
|
510
|
+
|
|
511
|
+
// Timestamp range (work hours: 9 AM to 5 PM)
|
|
512
|
+
const workHours = logs.where({
|
|
513
|
+
timestamp: dateTimeBetween(
|
|
514
|
+
'2024-01-15 09:00:00',
|
|
515
|
+
'2024-01-15 17:00:00'
|
|
516
|
+
)
|
|
517
|
+
});
|
|
518
|
+
// Result: Process
|
|
519
|
+
|
|
520
|
+
// Reactive datetime filtering
|
|
521
|
+
const startTime = Observable('2024-01-15 08:00:00');
|
|
522
|
+
const endTime = Observable('2024-01-15 16:00:00');
|
|
523
|
+
|
|
524
|
+
const filtered = logs.where({
|
|
525
|
+
timestamp: dateTimeBetween(startTime, endTime)
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Updates when times change
|
|
529
|
+
endTime.set('2024-01-15 20:00:00');
|
|
530
|
+
// Now includes all logs
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Date Format Examples
|
|
534
|
+
|
|
535
|
+
All date/time filters accept multiple formats:
|
|
536
|
+
```javascript
|
|
537
|
+
import { filters } from 'native-document/utils';
|
|
538
|
+
const { dateEquals, timeEquals, dateTimeEquals } = filters;
|
|
539
|
+
|
|
540
|
+
const events = Observable.array([...]);
|
|
541
|
+
|
|
542
|
+
// ISO 8601 string
|
|
543
|
+
events.where({ date: dateEquals('2024-01-15') });
|
|
544
|
+
|
|
545
|
+
// Date object
|
|
546
|
+
events.where({ date: dateEquals(new Date('2024-01-15')) });
|
|
547
|
+
|
|
548
|
+
// Timestamp (milliseconds)
|
|
549
|
+
events.where({ date: dateEquals(1705276800000) });
|
|
550
|
+
|
|
551
|
+
// Full datetime string
|
|
552
|
+
events.where({ timestamp: dateTimeEquals('2024-01-15T14:30:00') });
|
|
553
|
+
|
|
554
|
+
// Time only (date portion ignored)
|
|
555
|
+
events.where({ time: timeEquals('14:30:00') });
|
|
556
|
+
|
|
557
|
+
// Observable with any format
|
|
558
|
+
const targetDate = Observable('2024-01-15');
|
|
559
|
+
events.where({ date: dateEquals(targetDate) });
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Working with Different Timezones
|
|
563
|
+
```javascript
|
|
564
|
+
import { filters } from 'native-document/utils';
|
|
565
|
+
const { dateTimeBetween } = filters;
|
|
566
|
+
import { Observable } from 'native-document';
|
|
567
|
+
|
|
568
|
+
const events = Observable.array([
|
|
569
|
+
{ name: 'Meeting', time: '2024-01-15T14:00:00Z' }, // UTC
|
|
570
|
+
{ name: 'Call', time: '2024-01-15T09:00:00-05:00' }, // EST
|
|
571
|
+
{ name: 'Workshop', time: '2024-01-15T16:00:00+01:00' } // CET
|
|
572
|
+
]);
|
|
573
|
+
|
|
574
|
+
// All dates are converted to Date objects internally
|
|
575
|
+
// Local timezone is used for comparison
|
|
576
|
+
const todayEvents = events.where({
|
|
577
|
+
time: dateTimeBetween(
|
|
578
|
+
'2024-01-15T00:00:00',
|
|
579
|
+
'2024-01-15T23:59:59'
|
|
580
|
+
)
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Practical Date/Time Examples
|
|
585
|
+
|
|
586
|
+
#### Filter Events by Today/This Week
|
|
587
|
+
```javascript
|
|
588
|
+
import { filters } from 'native-document/utils';
|
|
589
|
+
const { dateEquals, dateBetween } = filters;
|
|
590
|
+
import { Observable } from 'native-document';
|
|
591
|
+
|
|
592
|
+
const events = Observable.array([...]);
|
|
593
|
+
|
|
594
|
+
// Today's events
|
|
595
|
+
const today = new Date().toISOString().split('T')[0]; // "2024-01-15"
|
|
596
|
+
const todayEvents = events.where({
|
|
597
|
+
date: dateEquals(today)
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// This week's events
|
|
601
|
+
const startOfWeek = new Date();
|
|
602
|
+
startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay());
|
|
603
|
+
|
|
604
|
+
const endOfWeek = new Date(startOfWeek);
|
|
605
|
+
endOfWeek.setDate(endOfWeek.getDate() + 6);
|
|
606
|
+
|
|
607
|
+
const thisWeekEvents = events.where({
|
|
608
|
+
date: dateBetween(
|
|
609
|
+
startOfWeek.toISOString().split('T')[0],
|
|
610
|
+
endOfWeek.toISOString().split('T')[0]
|
|
611
|
+
)
|
|
612
|
+
});
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
#### Filter by Business Hours
|
|
616
|
+
```javascript
|
|
617
|
+
import { filters } from 'native-document/utils';
|
|
618
|
+
const { timeBetween } = filters;
|
|
619
|
+
import { Observable } from 'native-document';
|
|
620
|
+
|
|
621
|
+
const calls = Observable.array([
|
|
622
|
+
{ caller: 'Alice', time: '2024-01-15 08:30:00' },
|
|
623
|
+
{ caller: 'Bob', time: '2024-01-15 14:30:00' },
|
|
624
|
+
{ caller: 'Charlie', time: '2024-01-15 20:00:00' }
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
// Business hours: 9 AM - 6 PM
|
|
628
|
+
const businessHoursCalls = calls.where({
|
|
629
|
+
time: timeBetween('09:00:00', '18:00:00')
|
|
630
|
+
});
|
|
631
|
+
// Result: Bob (14:30 is within business hours)
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
#### Filter Recent Activity
|
|
635
|
+
```javascript
|
|
636
|
+
import { filters } from 'native-document/utils';
|
|
637
|
+
const { dateTimeAfter } = filters;
|
|
638
|
+
import { Observable } from 'native-document';
|
|
639
|
+
|
|
640
|
+
const activities = Observable.array([...]);
|
|
641
|
+
|
|
642
|
+
// Last 24 hours
|
|
643
|
+
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
644
|
+
const recentActivity = activities.where({
|
|
645
|
+
timestamp: dateTimeAfter(oneDayAgo)
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Last hour
|
|
649
|
+
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
|
650
|
+
const veryRecentActivity = activities.where({
|
|
651
|
+
timestamp: dateTimeAfter(oneHourAgo)
|
|
652
|
+
});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Time Comparison (Ignores Date)
|
|
656
|
+
```javascript
|
|
657
|
+
import { filters } from 'native-document/utils';
|
|
658
|
+
const { timeEquals, timeBefore, timeAfter, timeBetween } = filters;
|
|
659
|
+
import { Observable } from 'native-document';
|
|
660
|
+
|
|
661
|
+
const appointments = Observable.array([
|
|
662
|
+
{ name: 'Breakfast', time: new Date('2024-01-15 08:00:00') },
|
|
663
|
+
{ name: 'Meeting', time: new Date('2024-01-15 14:00:00') },
|
|
664
|
+
{ name: 'Dinner', time: new Date('2024-01-15 19:00:00') }
|
|
665
|
+
]);
|
|
666
|
+
|
|
667
|
+
// Specific time (ignores date)
|
|
668
|
+
const lunchTime = appointments.where({
|
|
669
|
+
time: timeEquals(new Date('2024-01-01 14:00:00'))
|
|
670
|
+
});
|
|
671
|
+
// Result: Meeting (even though date is different)
|
|
672
|
+
|
|
673
|
+
// Before a time
|
|
674
|
+
const morning = appointments.where({
|
|
675
|
+
time: timeBefore(new Date('2024-01-01 12:00:00'))
|
|
676
|
+
});
|
|
677
|
+
// Result: Breakfast
|
|
678
|
+
|
|
679
|
+
// After a time
|
|
680
|
+
const evening = appointments.where({
|
|
681
|
+
time: timeAfter(new Date('2024-01-01 18:00:00'))
|
|
682
|
+
});
|
|
683
|
+
// Result: Dinner
|
|
684
|
+
|
|
685
|
+
// Time range
|
|
686
|
+
const businessHours = appointments.where({
|
|
687
|
+
time: timeBetween(
|
|
688
|
+
new Date('2024-01-01 09:00:00'),
|
|
689
|
+
new Date('2024-01-01 17:00:00')
|
|
690
|
+
)
|
|
691
|
+
});
|
|
692
|
+
// Result: Meeting
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### DateTime Comparison (Date + Time)
|
|
696
|
+
```javascript
|
|
697
|
+
import { filters } from 'native-document/utils';
|
|
698
|
+
const { dateTimeEquals, dateTimeBefore, dateTimeAfter, dateTimeBetween } = filters;
|
|
699
|
+
import { Observable } from 'native-document';
|
|
700
|
+
|
|
701
|
+
const logs = Observable.array([
|
|
702
|
+
{ message: 'Start', timestamp: new Date('2024-01-15 08:30:00') },
|
|
703
|
+
{ message: 'Process', timestamp: new Date('2024-01-15 14:45:00') },
|
|
704
|
+
{ message: 'End', timestamp: new Date('2024-01-15 18:20:00') }
|
|
705
|
+
]);
|
|
706
|
+
|
|
707
|
+
// Exact timestamp
|
|
708
|
+
const exactLog = logs.where({
|
|
709
|
+
timestamp: dateTimeEquals(new Date('2024-01-15 14:45:00'))
|
|
710
|
+
});
|
|
711
|
+
// Result: Process
|
|
712
|
+
|
|
713
|
+
// Before timestamp
|
|
714
|
+
const earlyLogs = logs.where({
|
|
715
|
+
timestamp: dateTimeBefore(new Date('2024-01-15 15:00:00'))
|
|
716
|
+
});
|
|
717
|
+
// Result: Start, Process
|
|
718
|
+
|
|
719
|
+
// After timestamp
|
|
720
|
+
const lateLogs = logs.where({
|
|
721
|
+
timestamp: dateTimeAfter(new Date('2024-01-15 15:00:00'))
|
|
722
|
+
});
|
|
723
|
+
// Result: End
|
|
724
|
+
|
|
725
|
+
// Timestamp range
|
|
726
|
+
const workHours = logs.where({
|
|
727
|
+
timestamp: dateTimeBetween(
|
|
728
|
+
new Date('2024-01-15 09:00:00'),
|
|
729
|
+
new Date('2024-01-15 17:00:00')
|
|
730
|
+
)
|
|
731
|
+
});
|
|
732
|
+
// Result: Process
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
## Logical Operators
|
|
736
|
+
|
|
737
|
+
### and() / all()
|
|
738
|
+
```javascript
|
|
739
|
+
import { filters } from 'native-document/utils';
|
|
740
|
+
const { and, all, greaterThan, lessThan } = filters;
|
|
741
|
+
import { Observable } from 'native-document';
|
|
742
|
+
|
|
743
|
+
const products = Observable.array([
|
|
744
|
+
{ name: 'Phone', price: 599, stock: 10 },
|
|
745
|
+
{ name: 'Laptop', price: 999, stock: 5 },
|
|
746
|
+
{ name: 'Tablet', price: 399, stock: 0 },
|
|
747
|
+
{ name: 'Watch', price: 299, stock: 15 }
|
|
748
|
+
]);
|
|
749
|
+
|
|
750
|
+
// Combine multiple conditions
|
|
751
|
+
const midRangeInStock = products.where({
|
|
752
|
+
price: and(
|
|
753
|
+
greaterThan(300),
|
|
754
|
+
lessThan(700)
|
|
755
|
+
),
|
|
756
|
+
stock: greaterThan(0)
|
|
757
|
+
});
|
|
758
|
+
// Result: Phone
|
|
759
|
+
|
|
760
|
+
// 'all' is an alias for 'and'
|
|
761
|
+
const sameResult = products.where({
|
|
762
|
+
price: all(
|
|
763
|
+
greaterThan(300),
|
|
764
|
+
lessThan(700)
|
|
765
|
+
)
|
|
766
|
+
});
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### or() / any()
|
|
770
|
+
```javascript
|
|
771
|
+
import { filters } from 'native-document/utils';
|
|
772
|
+
const { or, any, lessThan, greaterThan } = filters;
|
|
773
|
+
import { Observable } from 'native-document';
|
|
774
|
+
|
|
775
|
+
const products = Observable.array([
|
|
776
|
+
{ name: 'Budget Phone', price: 199 },
|
|
777
|
+
{ name: 'Mid Phone', price: 499 },
|
|
778
|
+
{ name: 'Premium Phone', price: 999 }
|
|
779
|
+
]);
|
|
780
|
+
|
|
781
|
+
// Either cheap OR expensive
|
|
782
|
+
const dealsOrPremium = products.where({
|
|
783
|
+
price: or(
|
|
784
|
+
lessThan(300),
|
|
785
|
+
greaterThan(800)
|
|
786
|
+
)
|
|
787
|
+
});
|
|
788
|
+
// Result: Budget Phone, Premium Phone
|
|
789
|
+
|
|
790
|
+
// 'any' is an alias for 'or'
|
|
791
|
+
const sameResult = products.where({
|
|
792
|
+
price: any(
|
|
793
|
+
lessThan(300),
|
|
794
|
+
greaterThan(800)
|
|
795
|
+
)
|
|
796
|
+
});
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### not()
|
|
800
|
+
```javascript
|
|
801
|
+
import { filters } from 'native-document/utils';
|
|
802
|
+
const { not, equals } = filters;
|
|
803
|
+
import { Observable } from 'native-document';
|
|
804
|
+
|
|
805
|
+
const users = Observable.array([
|
|
806
|
+
{ name: 'Alice', status: 'active' },
|
|
807
|
+
{ name: 'Bob', status: 'inactive' },
|
|
808
|
+
{ name: 'Charlie', status: 'active' }
|
|
809
|
+
]);
|
|
810
|
+
|
|
811
|
+
// Invert condition
|
|
812
|
+
const notActive = users.where({
|
|
813
|
+
status: not(equals('active'))
|
|
814
|
+
});
|
|
815
|
+
// Result: Bob
|
|
816
|
+
|
|
817
|
+
// Can combine with other filters
|
|
818
|
+
const notActiveOrBanned = users.where({
|
|
819
|
+
status: not(inArray(['active', 'banned']))
|
|
820
|
+
});
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
## Complex Filtering
|
|
824
|
+
|
|
825
|
+
### Nested Conditions
|
|
826
|
+
```javascript
|
|
827
|
+
import { filters } from 'native-document/utils';
|
|
828
|
+
const { and, or, greaterThan, lessThan, equals } = filters;
|
|
829
|
+
import { Observable } from 'native-document';
|
|
830
|
+
|
|
831
|
+
const products = Observable.array([
|
|
832
|
+
{ name: 'Phone', price: 599, category: 'electronics', stock: 10 },
|
|
833
|
+
{ name: 'Book', price: 29, category: 'books', stock: 50 },
|
|
834
|
+
{ name: 'Laptop', price: 999, category: 'electronics', stock: 5 },
|
|
835
|
+
{ name: 'Magazine', price: 9, category: 'books', stock: 100 }
|
|
836
|
+
]);
|
|
837
|
+
|
|
838
|
+
// (electronics AND expensive) OR (books AND cheap)
|
|
839
|
+
const filtered = products.where({
|
|
840
|
+
_: or(
|
|
841
|
+
and(
|
|
842
|
+
(item) => item.category === 'electronics',
|
|
843
|
+
(item) => item.price > 500
|
|
844
|
+
),
|
|
845
|
+
and(
|
|
846
|
+
(item) => item.category === 'books',
|
|
847
|
+
(item) => item.price < 20
|
|
848
|
+
)
|
|
849
|
+
)
|
|
850
|
+
});
|
|
851
|
+
// Result: Phone, Laptop, Magazine
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Multiple Property Filters
|
|
855
|
+
```javascript
|
|
856
|
+
import { filters } from 'native-document/utils';
|
|
857
|
+
const { greaterThan, includes, equals } = filters;
|
|
858
|
+
import { Observable } from 'native-document';
|
|
859
|
+
|
|
860
|
+
const products = Observable.array([
|
|
861
|
+
{ name: 'Gaming Phone', price: 799, category: 'electronics', tags: ['gaming', 'mobile'] },
|
|
862
|
+
{ name: 'Office Laptop', price: 1299, category: 'electronics', tags: ['work', 'productivity'] },
|
|
863
|
+
{ name: 'Budget Tablet', price: 299, category: 'electronics', tags: ['entertainment'] }
|
|
864
|
+
]);
|
|
865
|
+
|
|
866
|
+
// Filter on multiple properties
|
|
867
|
+
const filtered = products.where({
|
|
868
|
+
price: greaterThan(500),
|
|
869
|
+
category: equals('electronics'),
|
|
870
|
+
name: includes('gaming')
|
|
871
|
+
});
|
|
872
|
+
// Result: Gaming Phone
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
### Reactive Multi-Condition Filters
|
|
876
|
+
```javascript
|
|
877
|
+
import { filters } from 'native-document/utils';
|
|
878
|
+
const { and, greaterThan, lessThan, includes, custom } = filters;
|
|
879
|
+
import { Observable } from 'native-document';
|
|
880
|
+
|
|
881
|
+
const products = Observable.array([...]);
|
|
882
|
+
|
|
883
|
+
// Reactive filter values
|
|
884
|
+
const searchTerm = Observable('');
|
|
885
|
+
const minPrice = Observable(0);
|
|
886
|
+
const maxPrice = Observable(10000);
|
|
887
|
+
const showInStockOnly = Observable(false);
|
|
888
|
+
|
|
889
|
+
const filtered = products.where({
|
|
890
|
+
name: includes(searchTerm),
|
|
891
|
+
price: and(
|
|
892
|
+
greaterThan(minPrice),
|
|
893
|
+
lessThan(maxPrice)
|
|
894
|
+
),
|
|
895
|
+
stock: custom((value, showInStock) => {
|
|
896
|
+
return !showInStock || value > 0;
|
|
897
|
+
}, showInStockOnly)
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// Updates automatically when any filter changes
|
|
901
|
+
searchTerm.set('phone');
|
|
902
|
+
minPrice.set(500);
|
|
903
|
+
maxPrice.set(1000);
|
|
904
|
+
showInStockOnly.set(true);
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
## Custom Filters
|
|
908
|
+
|
|
909
|
+
### createFilter()
|
|
910
|
+
```javascript
|
|
911
|
+
import { filters } from 'native-document/utils';
|
|
912
|
+
const { createFilter } = filters;
|
|
913
|
+
import { Observable } from 'native-document';
|
|
914
|
+
|
|
915
|
+
// Create email validator
|
|
916
|
+
const isValidEmail = createFilter(
|
|
917
|
+
true, // static value or observable
|
|
918
|
+
(value, shouldBeValid) => {
|
|
919
|
+
const isValid = /\S+@\S+\.\S+/.test(value);
|
|
920
|
+
return shouldBeValid ? isValid : !isValid;
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
const users = Observable.array([
|
|
925
|
+
{ email: 'alice@example.com' },
|
|
926
|
+
{ email: 'invalid-email' },
|
|
927
|
+
{ email: 'bob@example.com' }
|
|
928
|
+
]);
|
|
929
|
+
|
|
930
|
+
const validUsers = users.where({
|
|
931
|
+
email: isValidEmail
|
|
932
|
+
});
|
|
933
|
+
// Result: alice@example.com, bob@example.com
|
|
934
|
+
|
|
935
|
+
// Reactive validation
|
|
936
|
+
const shouldValidate = Observable(true);
|
|
937
|
+
const emailFilter = createFilter(
|
|
938
|
+
shouldValidate,
|
|
939
|
+
(value, validate) => {
|
|
940
|
+
if (!validate) return true; // Skip validation
|
|
941
|
+
return /\S+@\S+\.\S+/.test(value);
|
|
942
|
+
}
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
const filtered = users.where({
|
|
946
|
+
email: emailFilter
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
shouldValidate.set(false);
|
|
950
|
+
// Now returns all users (validation disabled)
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
### createMultiSourceFilter()
|
|
954
|
+
```javascript
|
|
955
|
+
import { filters } from 'native-document/utils';
|
|
956
|
+
const { createMultiSourceFilter } = filters;
|
|
957
|
+
import { Observable } from 'native-document';
|
|
958
|
+
|
|
959
|
+
const minValue = Observable(0);
|
|
960
|
+
const maxValue = Observable(100);
|
|
961
|
+
const multiplier = Observable(1);
|
|
962
|
+
|
|
963
|
+
// Filter using multiple observables
|
|
964
|
+
const complexFilter = createMultiSourceFilter(
|
|
965
|
+
[minValue, maxValue, multiplier],
|
|
966
|
+
(value, [min, max, mult]) => {
|
|
967
|
+
const adjusted = value * mult;
|
|
968
|
+
return adjusted >= min && adjusted <= max;
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
const numbers = Observable.array([
|
|
973
|
+
{ value: 10 },
|
|
974
|
+
{ value: 50 },
|
|
975
|
+
{ value: 150 }
|
|
976
|
+
]);
|
|
977
|
+
|
|
978
|
+
const filtered = numbers.where({
|
|
979
|
+
value: complexFilter
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
// All observables update the filter
|
|
983
|
+
multiplier.set(2); // Now filters based on value * 2
|
|
984
|
+
minValue.set(50); // Now requires value * 2 >= 50
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### custom()
|
|
988
|
+
```javascript
|
|
989
|
+
import { filters } from 'native-document/utils';
|
|
990
|
+
const { custom } = filters;
|
|
991
|
+
import { Observable } from 'native-document';
|
|
992
|
+
|
|
993
|
+
const products = Observable.array([
|
|
994
|
+
{ name: 'Phone', price: 599, discount: 0.1 },
|
|
995
|
+
{ name: 'Laptop', price: 999, discount: 0.15 },
|
|
996
|
+
{ name: 'Tablet', price: 399, discount: 0.05 }
|
|
997
|
+
]);
|
|
998
|
+
|
|
999
|
+
const maxBudget = Observable(600);
|
|
1000
|
+
|
|
1001
|
+
// Custom filter with observable dependency
|
|
1002
|
+
const withinBudget = products.where({
|
|
1003
|
+
_: custom((product, budget) => {
|
|
1004
|
+
const finalPrice = product.price * (1 - product.discount);
|
|
1005
|
+
return finalPrice <= budget;
|
|
1006
|
+
}, maxBudget)
|
|
1007
|
+
});
|
|
1008
|
+
// Result: Phone, Tablet
|
|
1009
|
+
|
|
1010
|
+
// Updates when budget changes
|
|
1011
|
+
maxBudget.set(400);
|
|
1012
|
+
// Result: Tablet only
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
## Filter Reference
|
|
1016
|
+
|
|
1017
|
+
### Comparison Filters
|
|
1018
|
+
|
|
1019
|
+
| Filter | Alias | Description | Example |
|
|
1020
|
+
|--------|-------|-------------|---------|
|
|
1021
|
+
| `equals(value)` | `eq(value)` | Exact match | `equals(10)` |
|
|
1022
|
+
| `notEquals(value)` | `neq(value)` | Not equal | `notEquals('test')` |
|
|
1023
|
+
| `greaterThan(value)` | `gt(value)` | Greater than | `gt(100)` |
|
|
1024
|
+
| `greaterThanOrEqual(value)` | `gte(value)` | Greater or equal | `gte(50)` |
|
|
1025
|
+
| `lessThan(value)` | `lt(value)` | Less than | `lt(1000)` |
|
|
1026
|
+
| `lessThanOrEqual(value)` | `lte(value)` | Less or equal | `lte(500)` |
|
|
1027
|
+
|
|
1028
|
+
### Range Filters
|
|
1029
|
+
|
|
1030
|
+
| Filter | Description | Example |
|
|
1031
|
+
|--------|-------------|---------|
|
|
1032
|
+
| `between(min, max)` | Value within range (inclusive) | `between(10, 100)` |
|
|
1033
|
+
|
|
1034
|
+
### String Filters
|
|
1035
|
+
|
|
1036
|
+
| Filter | Description | Example |
|
|
1037
|
+
|--------|-------------|---------|
|
|
1038
|
+
| `includes(text, caseSensitive?)` | Contains substring | `includes('hello')` |
|
|
1039
|
+
| `contains(text, caseSensitive?)` | Alias for includes | `contains('world')` |
|
|
1040
|
+
| `startsWith(text, caseSensitive?)` | Starts with prefix | `startsWith('Mr')` |
|
|
1041
|
+
| `endsWith(text, caseSensitive?)` | Ends with suffix | `endsWith('.pdf')` |
|
|
1042
|
+
| `match(pattern, asRegex?, flags?)` | Pattern matching | `match(/\d+/, true)` |
|
|
1043
|
+
|
|
1044
|
+
### Array Filters
|
|
1045
|
+
|
|
1046
|
+
| Filter | Description | Example |
|
|
1047
|
+
|--------|-------------|---------|
|
|
1048
|
+
| `inArray(array)` | Value in array | `inArray(['a', 'b'])` |
|
|
1049
|
+
| `notIn(array)` | Value not in array | `notIn(['banned'])` |
|
|
1050
|
+
|
|
1051
|
+
### Empty Filters
|
|
1052
|
+
|
|
1053
|
+
| Filter | Description | Example |
|
|
1054
|
+
|--------|-------------|---------|
|
|
1055
|
+
| `isEmpty(shouldBeEmpty?)` | Value is empty/null | `isEmpty()` |
|
|
1056
|
+
| `isNotEmpty(shouldBeNotEmpty?)` | Value is not empty | `isNotEmpty()` |
|
|
1057
|
+
|
|
1058
|
+
### Date Filters
|
|
1059
|
+
|
|
1060
|
+
| Filter | Description | Example |
|
|
1061
|
+
|--------|-------------|---------|
|
|
1062
|
+
| `dateEquals(date)` | Same date (ignores time) | `dateEquals(new Date())` |
|
|
1063
|
+
| `dateBefore(date)` | Before date | `dateBefore(new Date())` |
|
|
1064
|
+
| `dateAfter(date)` | After date | `dateAfter(new Date())` |
|
|
1065
|
+
| `dateBetween(start, end)` | Date range | `dateBetween(start, end)` |
|
|
1066
|
+
|
|
1067
|
+
### Time Filters
|
|
1068
|
+
|
|
1069
|
+
| Filter | Description | Example |
|
|
1070
|
+
|--------|-------------|---------|
|
|
1071
|
+
| `timeEquals(time)` | Same time (ignores date) | `timeEquals(new Date())` |
|
|
1072
|
+
| `timeBefore(time)` | Before time | `timeBefore(new Date())` |
|
|
1073
|
+
| `timeAfter(time)` | After time | `timeAfter(new Date())` |
|
|
1074
|
+
| `timeBetween(start, end)` | Time range | `timeBetween(start, end)` |
|
|
1075
|
+
|
|
1076
|
+
### DateTime Filters
|
|
1077
|
+
|
|
1078
|
+
| Filter | Description | Example |
|
|
1079
|
+
|--------|-------------|---------|
|
|
1080
|
+
| `dateTimeEquals(datetime)` | Exact timestamp | `dateTimeEquals(new Date())` |
|
|
1081
|
+
| `dateTimeBefore(datetime)` | Before timestamp | `dateTimeBefore(new Date())` |
|
|
1082
|
+
| `dateTimeAfter(datetime)` | After timestamp | `dateTimeAfter(new Date())` |
|
|
1083
|
+
| `dateTimeBetween(start, end)` | Timestamp range | `dateTimeBetween(start, end)` |
|
|
1084
|
+
|
|
1085
|
+
### Logical Operators
|
|
1086
|
+
|
|
1087
|
+
| Filter | Alias | Description | Example |
|
|
1088
|
+
|--------|-------|-------------|---------|
|
|
1089
|
+
| `and(...filters)` | `all(...filters)` | All conditions must match | `and(gt(10), lt(100))` |
|
|
1090
|
+
| `or(...filters)` | `any(...filters)` | Any condition must match | `or(eq('a'), eq('b'))` |
|
|
1091
|
+
| `not(filter)` | - | Invert condition | `not(equals('test'))` |
|
|
1092
|
+
|
|
1093
|
+
### Custom Filters
|
|
1094
|
+
|
|
1095
|
+
| Filter | Description | Example |
|
|
1096
|
+
|--------|-------------|---------|
|
|
1097
|
+
| `createFilter(value, callback)` | Single source custom filter | See above |
|
|
1098
|
+
| `createMultiSourceFilter(sources, callback)` | Multi-source custom filter | See above |
|
|
1099
|
+
| `custom(callback, ...observables)` | Custom logic with dependencies | See above |
|
|
1100
|
+
|
|
1101
|
+
## Best Practices
|
|
1102
|
+
|
|
1103
|
+
### 1. Use Specific Filters
|
|
1104
|
+
```javascript
|
|
1105
|
+
import { filters } from 'native-document/utils';
|
|
1106
|
+
const { equals, greaterThan } = filters;
|
|
1107
|
+
|
|
1108
|
+
// ✅ Good: Specific property filters
|
|
1109
|
+
const filtered = products.where({
|
|
1110
|
+
price: greaterThan(100),
|
|
1111
|
+
category: equals('electronics')
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// ❌ Less efficient: Generic filter
|
|
1115
|
+
const filtered = products.where({
|
|
1116
|
+
_: (product) => product.price > 100 && product.category === 'electronics'
|
|
1117
|
+
});
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
### 2. Reuse Observable Filters
|
|
1121
|
+
```javascript
|
|
1122
|
+
import { filters } from 'native-document/utils';
|
|
1123
|
+
const { between } = filters;
|
|
1124
|
+
import { Observable } from 'native-document';
|
|
1125
|
+
|
|
1126
|
+
// ✅ Good: Reuse observables
|
|
1127
|
+
const minPrice = Observable(0);
|
|
1128
|
+
const maxPrice = Observable(1000);
|
|
1129
|
+
|
|
1130
|
+
const products1Filtered = products1.where({ price: between(minPrice, maxPrice) });
|
|
1131
|
+
const products2Filtered = products2.where({ price: between(minPrice, maxPrice) });
|
|
1132
|
+
|
|
1133
|
+
// Both update when observables change
|
|
1134
|
+
minPrice.set(500);
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
### 3. Combine Related Filters
|
|
1138
|
+
```javascript
|
|
1139
|
+
import { filters } from 'native-document/utils';
|
|
1140
|
+
const { and, greaterThan, lessThan } = filters;
|
|
1141
|
+
|
|
1142
|
+
// ✅ Good: Use 'and' for multiple conditions
|
|
1143
|
+
const filtered = products.where({
|
|
1144
|
+
price: and(greaterThan(100), lessThan(500))
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// ❌ Bad: Multiple where() calls
|
|
1148
|
+
const filtered = products
|
|
1149
|
+
.where({ price: greaterThan(100) })
|
|
1150
|
+
.where({ price: lessThan(500) });
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
### 4. Document Complex Filters
|
|
1154
|
+
```javascript
|
|
1155
|
+
import { filters } from 'native-document/utils';
|
|
1156
|
+
const { and, or, greaterThan, equals } = filters;
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Filters products for flash sale eligibility:
|
|
1160
|
+
* - In stock OR coming soon
|
|
1161
|
+
* - Price between $50-$500
|
|
1162
|
+
* - High rating (4+ stars)
|
|
1163
|
+
*/
|
|
1164
|
+
const flashSaleProducts = products.where({
|
|
1165
|
+
stock: or(greaterThan(0), equals('coming-soon')),
|
|
1166
|
+
price: and(greaterThan(50), lessThan(500)),
|
|
1167
|
+
rating: greaterThan(4)
|
|
1168
|
+
});
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
### 5. Avoid Over-Filtering
|
|
1172
|
+
```javascript
|
|
1173
|
+
import { filters } from 'native-document/utils';
|
|
1174
|
+
const { equals, greaterThan } = filters;
|
|
1175
|
+
|
|
1176
|
+
// ❌ Bad: Too many where() calls
|
|
1177
|
+
const filtered = products
|
|
1178
|
+
.where({ category: equals('electronics') })
|
|
1179
|
+
.where({ price: greaterThan(100) })
|
|
1180
|
+
.where({ stock: greaterThan(0) })
|
|
1181
|
+
.where({ rating: greaterThan(4) });
|
|
1182
|
+
|
|
1183
|
+
// ✅ Good: Combine into single where
|
|
1184
|
+
const filtered = products.where({
|
|
1185
|
+
category: equals('electronics'),
|
|
1186
|
+
price: greaterThan(100),
|
|
1187
|
+
stock: greaterThan(0),
|
|
1188
|
+
rating: greaterThan(4)
|
|
1189
|
+
});
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
## Next Steps
|
|
1193
|
+
|
|
1194
|
+
Explore related utilities and concepts:
|
|
1195
|
+
|
|
1196
|
+
## Next Steps
|
|
1197
|
+
|
|
1198
|
+
- **[Getting Started](getting-started.md)** - Installation and first steps
|
|
1199
|
+
- **[Core Concepts](core-concepts.md)** - Understanding the fundamentals
|
|
1200
|
+
- **[Observables](observables.md)** - Reactive state management
|
|
1201
|
+
- **[Elements](elements.md)** - Creating and composing UI
|
|
1202
|
+
- **[Conditional Rendering](conditional-rendering.md)** - Dynamic content
|
|
1203
|
+
- **[List Rendering](list-rendering.md)** - (ForEach | ForEachArray) and dynamic lists
|
|
1204
|
+
- **[Routing](routing.md)** - Navigation and URL management
|
|
1205
|
+
- **[State Management](state-management.md)** - Global state patterns
|
|
1206
|
+
- **[NDElement](native-document-element.md)** - Native Document Element
|
|
1207
|
+
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
1208
|
+
- **[Advanced Components](advanced-components.md)** - Template caching and singleton views
|
|
1209
|
+
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
1210
|
+
- **[Memory Management](memory-management.md)** - Memory management
|
|
1211
|
+
|
|
1212
|
+
## Utilities
|
|
1213
|
+
|
|
1214
|
+
- **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
|
|
1215
|
+
- **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
|
|
1216
|
+
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|