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/cache.md
ADDED
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
# Cache Utilities
|
|
2
|
+
|
|
3
|
+
NativeDocument provides utility functions for optimizing function execution through lazy initialization, memoization, and singleton patterns. These utilities help improve performance by deferring execution and caching results.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Cache utilities include:
|
|
8
|
+
- **`Cache.once(fn)`** - Lazy execution (via `autoOnce`) - executes on first property access
|
|
9
|
+
- **`Cache.singleton(fn)`** - Eager singleton (via `once`) - executes immediately on first call
|
|
10
|
+
- **`Cache.memoize(fn)`** - Lazy memoization (via `autoMemoize`) - proxy-based caching
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
```javascript
|
|
14
|
+
import { Cache } from 'native-document/utils';
|
|
15
|
+
|
|
16
|
+
// Use Cache methods
|
|
17
|
+
const lazyInit = Cache.once(() => { /* ... */ });
|
|
18
|
+
const singleton = Cache.singleton(() => { /* ... */ });
|
|
19
|
+
const memoized = Cache.memoize((key) => { /* ... */ });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Cache.once() - Lazy Initialization
|
|
23
|
+
|
|
24
|
+
Lazy execution using `autoOnce`. The function is **not executed immediately** - it only runs when you access a property on the returned object.
|
|
25
|
+
|
|
26
|
+
### Basic Usage
|
|
27
|
+
```javascript
|
|
28
|
+
import { Cache } from 'native-document/utils';
|
|
29
|
+
|
|
30
|
+
const LazyConfig = Cache.once(() => {
|
|
31
|
+
console.log('Loading config...');
|
|
32
|
+
return {
|
|
33
|
+
apiUrl: 'https://api.example.com',
|
|
34
|
+
timeout: 5000,
|
|
35
|
+
maxRetries: 3
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Function not executed yet
|
|
40
|
+
console.log('Config created');
|
|
41
|
+
|
|
42
|
+
// First property access triggers execution
|
|
43
|
+
console.log(LazyConfig.apiUrl);
|
|
44
|
+
// Logs: "Loading config..."
|
|
45
|
+
// Returns: "https://api.example.com"
|
|
46
|
+
|
|
47
|
+
// Subsequent accesses use cached result
|
|
48
|
+
console.log(LazyConfig.timeout); // No log, returns 5000
|
|
49
|
+
console.log(LazyConfig.maxRetries); // No log, returns 3
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Lazy Module Loading
|
|
53
|
+
```javascript
|
|
54
|
+
import { Cache } from 'native-document/utils';
|
|
55
|
+
|
|
56
|
+
const Utils = Cache.once(() => {
|
|
57
|
+
console.log('Initializing utils...');
|
|
58
|
+
return {
|
|
59
|
+
formatDate: (date) => new Date(date).toLocaleDateString(),
|
|
60
|
+
capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),
|
|
61
|
+
truncate: (str, len) => str.length > len ? str.slice(0, len) + '...' : str,
|
|
62
|
+
slugify: (str) => str.toLowerCase().replace(/\s+/g, '-')
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Utils not initialized yet
|
|
67
|
+
console.log('App started');
|
|
68
|
+
|
|
69
|
+
// First use initializes the module
|
|
70
|
+
const formatted = Utils.formatDate(new Date());
|
|
71
|
+
// Logs: "Initializing utils..."
|
|
72
|
+
|
|
73
|
+
// Already initialized - no log
|
|
74
|
+
const capitalized = Utils.capitalize('hello');
|
|
75
|
+
const slug = Utils.slugify('Hello World');
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Lazy API Client
|
|
79
|
+
```javascript
|
|
80
|
+
import { Cache } from 'native-document/utils';
|
|
81
|
+
import { NativeFetch } from 'native-document/utils';
|
|
82
|
+
|
|
83
|
+
const API = Cache.once(() => {
|
|
84
|
+
console.log('Setting up API client...');
|
|
85
|
+
const client = new NativeFetch('https://api.example.com');
|
|
86
|
+
|
|
87
|
+
client.interceptors.request((config) => {
|
|
88
|
+
config.headers['Authorization'] = `Bearer ${getToken()}`;
|
|
89
|
+
return config;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
users: {
|
|
94
|
+
get: (id) => client.get(`/users/${id}`),
|
|
95
|
+
list: () => client.get('/users')
|
|
96
|
+
},
|
|
97
|
+
posts: {
|
|
98
|
+
get: (id) => client.get(`/posts/${id}`),
|
|
99
|
+
create: (data) => client.post('/posts', data)
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Client not created yet
|
|
105
|
+
console.log('Starting app...');
|
|
106
|
+
|
|
107
|
+
// Client created on first API call
|
|
108
|
+
const users = await API.users.list();
|
|
109
|
+
// Logs: "Setting up API client..."
|
|
110
|
+
|
|
111
|
+
// Client already created
|
|
112
|
+
const user = await API.users.get('123');
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Cache.singleton() - Eager Singleton
|
|
116
|
+
|
|
117
|
+
Eager execution using `once`. The function executes **on the first call** and caches the result for subsequent calls.
|
|
118
|
+
|
|
119
|
+
### Basic Usage
|
|
120
|
+
```javascript
|
|
121
|
+
import { Cache } from 'native-document/utils';
|
|
122
|
+
|
|
123
|
+
const getLogger = Cache.singleton(() => {
|
|
124
|
+
console.log('Creating logger instance...');
|
|
125
|
+
return {
|
|
126
|
+
log: (msg) => console.log(`[LOG] ${msg}`),
|
|
127
|
+
error: (msg) => console.error(`[ERROR] ${msg}`),
|
|
128
|
+
warn: (msg) => console.warn(`[WARN] ${msg}`),
|
|
129
|
+
info: (msg) => console.info(`[INFO] ${msg}`)
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// First call creates the logger
|
|
134
|
+
const logger = getLogger();
|
|
135
|
+
// Logs: "Creating logger instance..."
|
|
136
|
+
|
|
137
|
+
logger.log('Application started');
|
|
138
|
+
|
|
139
|
+
// Subsequent calls return cached instance
|
|
140
|
+
const logger2 = getLogger();
|
|
141
|
+
// No log - returns cached instance
|
|
142
|
+
|
|
143
|
+
console.log(logger === logger2); // true - same reference
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Application Configuration
|
|
147
|
+
```javascript
|
|
148
|
+
import { Cache } from 'native-document/utils';
|
|
149
|
+
|
|
150
|
+
const getConfig = Cache.singleton(() => {
|
|
151
|
+
console.log('Loading configuration...');
|
|
152
|
+
return {
|
|
153
|
+
apiUrl: import.meta.env.VITE_API_URL || 'https://api.example.com',
|
|
154
|
+
debug: import.meta.env.DEV,
|
|
155
|
+
version: '1.0.0',
|
|
156
|
+
features: {
|
|
157
|
+
analytics: true,
|
|
158
|
+
darkMode: true
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// First call loads config
|
|
164
|
+
const config = getConfig();
|
|
165
|
+
// Logs: "Loading configuration..."
|
|
166
|
+
|
|
167
|
+
console.log(config.apiUrl);
|
|
168
|
+
|
|
169
|
+
// Returns same config instance
|
|
170
|
+
const config2 = getConfig();
|
|
171
|
+
console.log(config === config2); // true
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Event Bus Singleton
|
|
175
|
+
```javascript
|
|
176
|
+
import { Cache } from 'native-document/utils';
|
|
177
|
+
|
|
178
|
+
const getEventBus = Cache.singleton(() => {
|
|
179
|
+
console.log('Creating event bus...');
|
|
180
|
+
const listeners = new Map();
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
on: (event, callback) => {
|
|
184
|
+
if (!listeners.has(event)) {
|
|
185
|
+
listeners.set(event, []);
|
|
186
|
+
}
|
|
187
|
+
listeners.get(event).push(callback);
|
|
188
|
+
},
|
|
189
|
+
off: (event, callback) => {
|
|
190
|
+
const eventListeners = listeners.get(event);
|
|
191
|
+
if (eventListeners) {
|
|
192
|
+
const index = eventListeners.indexOf(callback);
|
|
193
|
+
if (index > -1) {
|
|
194
|
+
eventListeners.splice(index, 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
emit: (event, data) => {
|
|
199
|
+
const eventListeners = listeners.get(event);
|
|
200
|
+
if (eventListeners) {
|
|
201
|
+
eventListeners.forEach(callback => callback(data));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Create event bus
|
|
208
|
+
const bus = getEventBus();
|
|
209
|
+
// Logs: "Creating event bus..."
|
|
210
|
+
|
|
211
|
+
bus.on('user:login', (user) => console.log('User logged in:', user));
|
|
212
|
+
bus.emit('user:login', { id: 1, name: 'John' });
|
|
213
|
+
|
|
214
|
+
// Same event bus everywhere
|
|
215
|
+
const bus2 = getEventBus();
|
|
216
|
+
console.log(bus === bus2); // true
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Cache.memoize() - Lazy Memoization with Key-Based Instances
|
|
220
|
+
|
|
221
|
+
Lazy memoization using `autoMemoize`. Each property access creates and caches a **separate instance** of the function result, using the property name as the key parameter.
|
|
222
|
+
|
|
223
|
+
### How It Works
|
|
224
|
+
```javascript
|
|
225
|
+
import { Cache } from 'native-document/utils';
|
|
226
|
+
|
|
227
|
+
const API = Cache.memoize((key) => {
|
|
228
|
+
console.log(`Creating API for: ${key}`);
|
|
229
|
+
return {
|
|
230
|
+
async list() {
|
|
231
|
+
return await fetch('/' + key);
|
|
232
|
+
},
|
|
233
|
+
async get(id) {
|
|
234
|
+
return await fetch('/' + key + '/' + id);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// First access to 'users' - executes function with key='users'
|
|
240
|
+
await API.users.list();
|
|
241
|
+
// Logs: "Creating API for: users"
|
|
242
|
+
// Fetch: '/users'
|
|
243
|
+
|
|
244
|
+
// Second access to 'users' - returns cached instance
|
|
245
|
+
await API.users.get('123');
|
|
246
|
+
// No log - cached instance
|
|
247
|
+
// Fetch: '/users/123'
|
|
248
|
+
|
|
249
|
+
// First access to 'posts' - executes function with key='posts'
|
|
250
|
+
await API.posts.list();
|
|
251
|
+
// Logs: "Creating API for: posts"
|
|
252
|
+
// Fetch: '/posts'
|
|
253
|
+
|
|
254
|
+
// Cached instance for 'posts'
|
|
255
|
+
await API.posts.get('456');
|
|
256
|
+
// No log - cached instance
|
|
257
|
+
// Fetch: '/posts/456'
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Key Concepts
|
|
261
|
+
|
|
262
|
+
1. **Property name becomes the key**: `API.users` → `key = 'users'`
|
|
263
|
+
2. **Function executed per unique key**: First access creates instance
|
|
264
|
+
3. **Results cached by key**: Subsequent accesses return same instance
|
|
265
|
+
4. **Each key has its own instance**: `API.users` ≠ `API.posts`
|
|
266
|
+
|
|
267
|
+
### Basic Resource Providers
|
|
268
|
+
```javascript
|
|
269
|
+
import { Cache } from 'native-document/utils';
|
|
270
|
+
|
|
271
|
+
const Resources = Cache.memoize((resource) => {
|
|
272
|
+
console.log(`Loading ${resource}...`);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
data: `${resource} data`,
|
|
276
|
+
load: () => console.log(`Reloading ${resource}`),
|
|
277
|
+
save: (content) => console.log(`Saving to ${resource}:`, content)
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// First access - creates 'icons' instance
|
|
282
|
+
Resources.icons.load();
|
|
283
|
+
// Logs: "Loading icons..."
|
|
284
|
+
// Logs: "Reloading icons"
|
|
285
|
+
|
|
286
|
+
// Cached instance
|
|
287
|
+
Resources.icons.save('new-icon.svg');
|
|
288
|
+
// Logs: "Saving to icons: new-icon.svg"
|
|
289
|
+
|
|
290
|
+
// Different key - creates 'fonts' instance
|
|
291
|
+
Resources.fonts.load();
|
|
292
|
+
// Logs: "Loading fonts..."
|
|
293
|
+
// Logs: "Reloading fonts"
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### API Endpoint Collections
|
|
297
|
+
```javascript
|
|
298
|
+
import { Cache } from 'native-document/utils';
|
|
299
|
+
import { NativeFetch } from 'native-document/utils';
|
|
300
|
+
|
|
301
|
+
const api = new NativeFetch('https://api.example.com');
|
|
302
|
+
|
|
303
|
+
const Endpoints = Cache.memoize((resource) => {
|
|
304
|
+
console.log(`Creating endpoint for: ${resource}`);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
list: () => api.get(`/${resource}`),
|
|
308
|
+
get: (id) => api.get(`/${resource}/${id}`),
|
|
309
|
+
create: (data) => api.post(`/${resource}`, data),
|
|
310
|
+
update: (id, data) => api.put(`/${resource}/${id}`, data),
|
|
311
|
+
delete: (id) => api.delete(`/${resource}/${id}`)
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Create 'users' endpoint instance
|
|
316
|
+
const users = await Endpoints.users.list();
|
|
317
|
+
// Logs: "Creating endpoint for: users"
|
|
318
|
+
// GET: /users
|
|
319
|
+
|
|
320
|
+
const user = await Endpoints.users.get('123');
|
|
321
|
+
// GET: /users/123 (cached instance)
|
|
322
|
+
|
|
323
|
+
// Create 'posts' endpoint instance
|
|
324
|
+
const posts = await Endpoints.posts.list();
|
|
325
|
+
// Logs: "Creating endpoint for: posts"
|
|
326
|
+
// GET: /posts
|
|
327
|
+
|
|
328
|
+
await Endpoints.posts.create({ title: 'New Post' });
|
|
329
|
+
// POST: /posts (cached instance)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Store Providers
|
|
333
|
+
```javascript
|
|
334
|
+
import { Cache } from 'native-document/utils';
|
|
335
|
+
import { Store } from 'native-document';
|
|
336
|
+
|
|
337
|
+
const Stores = Cache.memoize((storeName) => {
|
|
338
|
+
console.log(`Creating store: ${storeName}`);
|
|
339
|
+
|
|
340
|
+
// Create store if it doesn't exist
|
|
341
|
+
if (!Store.get(storeName)) {
|
|
342
|
+
Store.create(storeName, {
|
|
343
|
+
items: [],
|
|
344
|
+
loading: false,
|
|
345
|
+
error: null
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
get: () => Store.use(storeName),
|
|
351
|
+
setLoading: (value) => {
|
|
352
|
+
/*...*/
|
|
353
|
+
},
|
|
354
|
+
addItem: (items) => {
|
|
355
|
+
/*...*/
|
|
356
|
+
},
|
|
357
|
+
setItems: (items) => {
|
|
358
|
+
/*...*/
|
|
359
|
+
},
|
|
360
|
+
setError: (error) => {
|
|
361
|
+
/*...*/
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Create 'products' store
|
|
367
|
+
const productsStore = Stores.products.get();
|
|
368
|
+
// Logs: "Creating store: products"
|
|
369
|
+
|
|
370
|
+
Stores.products.setLoading(true);
|
|
371
|
+
Stores.products.addItem({ id: 1, name: 'Product 1' });
|
|
372
|
+
|
|
373
|
+
// Create 'users' store
|
|
374
|
+
const usersStore = Stores.users.get();
|
|
375
|
+
// Logs: "Creating store: users"
|
|
376
|
+
|
|
377
|
+
Stores.users.setItems([{ id: 1, name: 'Alice' }]);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### LocalStorage Namespaces
|
|
381
|
+
```javascript
|
|
382
|
+
import { Cache } from 'native-document/utils';
|
|
383
|
+
|
|
384
|
+
const Storage = Cache.memoize((namespace) => {
|
|
385
|
+
console.log(`Creating storage for namespace: ${namespace}`);
|
|
386
|
+
|
|
387
|
+
const prefix = `${namespace}:`;
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
get: (key) => {
|
|
391
|
+
try {
|
|
392
|
+
const item = localStorage.getItem(prefix + key);
|
|
393
|
+
return item ? JSON.parse(item) : null;
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
set: (key, value) => {
|
|
399
|
+
try {
|
|
400
|
+
localStorage.setItem(prefix + key, JSON.stringify(value));
|
|
401
|
+
return true;
|
|
402
|
+
} catch {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
remove: (key) => {
|
|
407
|
+
localStorage.removeItem(prefix + key);
|
|
408
|
+
},
|
|
409
|
+
clear: () => {
|
|
410
|
+
// Clear all keys with this namespace
|
|
411
|
+
Object.keys(localStorage)
|
|
412
|
+
.filter(key => key.startsWith(prefix))
|
|
413
|
+
.forEach(key => localStorage.removeItem(key));
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// User preferences namespace
|
|
419
|
+
Storage.user.set('theme', 'dark');
|
|
420
|
+
// Logs: "Creating storage for namespace: user"
|
|
421
|
+
// localStorage: "user:theme" = "dark"
|
|
422
|
+
|
|
423
|
+
Storage.user.set('language', 'en');
|
|
424
|
+
// localStorage: "user:language" = "en"
|
|
425
|
+
|
|
426
|
+
const theme = Storage.user.get('theme');
|
|
427
|
+
// Returns: "dark"
|
|
428
|
+
|
|
429
|
+
// App settings namespace (different instance)
|
|
430
|
+
Storage.app.set('version', '1.0.0');
|
|
431
|
+
// Logs: "Creating storage for namespace: app"
|
|
432
|
+
// localStorage: "app:version" = "1.0.0"
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Event Handlers by Type
|
|
436
|
+
```javascript
|
|
437
|
+
import { Cache } from 'native-document/utils';
|
|
438
|
+
|
|
439
|
+
const EventHandlers = Cache.memoize((eventType) => {
|
|
440
|
+
console.log(`Creating handler for: ${eventType}`);
|
|
441
|
+
const listeners = [];
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
add: (callback) => {
|
|
445
|
+
listeners.push(callback);
|
|
446
|
+
},
|
|
447
|
+
remove: (callback) => {
|
|
448
|
+
const index = listeners.indexOf(callback);
|
|
449
|
+
if (index > -1) {
|
|
450
|
+
listeners.splice(index, 1);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
emit: (data) => {
|
|
454
|
+
listeners.forEach(callback => callback(data));
|
|
455
|
+
},
|
|
456
|
+
count: () => listeners.length
|
|
457
|
+
};
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Create 'click' event handler
|
|
461
|
+
EventHandlers.click.add((data) => console.log('Clicked:', data));
|
|
462
|
+
// Logs: "Creating handler for: click"
|
|
463
|
+
|
|
464
|
+
EventHandlers.click.emit({ x: 100, y: 200 });
|
|
465
|
+
// Logs: "Clicked: { x: 100, y: 200 }"
|
|
466
|
+
|
|
467
|
+
// Create 'scroll' event handler (different instance)
|
|
468
|
+
EventHandlers.scroll.add((data) => console.log('Scrolled:', data));
|
|
469
|
+
// Logs: "Creating handler for: scroll"
|
|
470
|
+
|
|
471
|
+
EventHandlers.scroll.emit({ top: 500 });
|
|
472
|
+
// Logs: "Scrolled: { top: 500 }"
|
|
473
|
+
|
|
474
|
+
console.log(EventHandlers.click.count()); // 1
|
|
475
|
+
console.log(EventHandlers.scroll.count()); // 1
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Validation Rules by Type
|
|
479
|
+
```javascript
|
|
480
|
+
import { Cache } from 'native-document/utils';
|
|
481
|
+
|
|
482
|
+
const Validators = Cache.memoize((fieldType) => {
|
|
483
|
+
console.log(`Creating validators for: ${fieldType}`);
|
|
484
|
+
|
|
485
|
+
const rules = {
|
|
486
|
+
email: {
|
|
487
|
+
required: (value) => !!value || 'Email is required',
|
|
488
|
+
format: (value) => /\S+@\S+\.\S+/.test(value) || 'Invalid email format'
|
|
489
|
+
},
|
|
490
|
+
password: {
|
|
491
|
+
required: (value) => !!value || 'Password is required',
|
|
492
|
+
minLength: (value) => value.length >= 8 || 'Password must be at least 8 characters',
|
|
493
|
+
hasNumber: (value) => /\d/.test(value) || 'Password must contain a number'
|
|
494
|
+
},
|
|
495
|
+
phone: {
|
|
496
|
+
required: (value) => !!value || 'Phone is required',
|
|
497
|
+
format: (value) => /^\d{10}$/.test(value) || 'Phone must be 10 digits'
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
validate: (value) => {
|
|
503
|
+
const fieldRules = rules[fieldType];
|
|
504
|
+
if (!fieldRules) return [];
|
|
505
|
+
|
|
506
|
+
const errors = [];
|
|
507
|
+
for (const [ruleName, rule] of Object.entries(fieldRules)) {
|
|
508
|
+
const result = rule(value);
|
|
509
|
+
if (result !== true) {
|
|
510
|
+
errors.push(result);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return errors;
|
|
514
|
+
},
|
|
515
|
+
isValid: (value) => {
|
|
516
|
+
return Validators[fieldType].validate(value).length === 0;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Validate email
|
|
522
|
+
const emailErrors = Validators.email.validate('invalid-email');
|
|
523
|
+
// Logs: "Creating validators for: email"
|
|
524
|
+
// Returns: ['Invalid email format']
|
|
525
|
+
|
|
526
|
+
const isEmailValid = Validators.email.isValid('test@example.com');
|
|
527
|
+
// Returns: true (cached instance)
|
|
528
|
+
|
|
529
|
+
// Validate password
|
|
530
|
+
const passwordErrors = Validators.password.validate('weak');
|
|
531
|
+
// Logs: "Creating validators for: password"
|
|
532
|
+
// Returns: ['Password must be at least 8 characters', 'Password must contain a number']
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Chart Instances by Type
|
|
536
|
+
```javascript
|
|
537
|
+
import { Cache } from 'native-document/utils';
|
|
538
|
+
|
|
539
|
+
const Charts = Cache.memoize((chartType) => {
|
|
540
|
+
console.log(`Creating ${chartType} chart instance...`);
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
render: (container, data, options = {}) => {
|
|
544
|
+
console.log(`Rendering ${chartType} chart`);
|
|
545
|
+
|
|
546
|
+
// Simplified chart rendering
|
|
547
|
+
const canvas = document.createElement('canvas');
|
|
548
|
+
container.appendChild(canvas);
|
|
549
|
+
|
|
550
|
+
// Chart-specific rendering logic
|
|
551
|
+
switch (chartType) {
|
|
552
|
+
case 'bar':
|
|
553
|
+
renderBarChart(canvas, data, options);
|
|
554
|
+
break;
|
|
555
|
+
case 'line':
|
|
556
|
+
renderLineChart(canvas, data, options);
|
|
557
|
+
break;
|
|
558
|
+
case 'pie':
|
|
559
|
+
renderPieChart(canvas, data, options);
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return canvas;
|
|
564
|
+
},
|
|
565
|
+
update: (canvas, data) => {
|
|
566
|
+
console.log(`Updating ${chartType} chart`);
|
|
567
|
+
// Update logic
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Create bar chart instance
|
|
573
|
+
const barContainer = document.querySelector('#bar-chart');
|
|
574
|
+
Charts.bar.render(barContainer, [10, 20, 30]);
|
|
575
|
+
// Logs: "Creating bar chart instance..."
|
|
576
|
+
// Logs: "Rendering bar chart"
|
|
577
|
+
|
|
578
|
+
// Create line chart instance (different from bar)
|
|
579
|
+
const lineContainer = document.querySelector('#line-chart');
|
|
580
|
+
Charts.line.render(lineContainer, [5, 15, 25]);
|
|
581
|
+
// Logs: "Creating line chart instance..."
|
|
582
|
+
// Logs: "Rendering line chart"
|
|
583
|
+
|
|
584
|
+
// Reuse bar chart instance
|
|
585
|
+
Charts.bar.update(barCanvas, [15, 25, 35]);
|
|
586
|
+
// Logs: "Updating bar chart"
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Form Field Managers
|
|
590
|
+
```javascript
|
|
591
|
+
import { Cache } from 'native-document/utils';
|
|
592
|
+
import { Observable } from 'native-document';
|
|
593
|
+
|
|
594
|
+
const FormFields = Cache.memoize((fieldName) => {
|
|
595
|
+
console.log(`Creating field manager for: ${fieldName}`);
|
|
596
|
+
|
|
597
|
+
const value = Observable('');
|
|
598
|
+
const errors = Observable([]);
|
|
599
|
+
const touched = Observable(false);
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
value,
|
|
603
|
+
errors,
|
|
604
|
+
touched,
|
|
605
|
+
setValue: (newValue) => {
|
|
606
|
+
value.set(newValue);
|
|
607
|
+
touched.set(true);
|
|
608
|
+
},
|
|
609
|
+
setErrors: (newErrors) => {
|
|
610
|
+
errors.set(newErrors);
|
|
611
|
+
},
|
|
612
|
+
reset: () => {
|
|
613
|
+
value.set('');
|
|
614
|
+
errors.set([]);
|
|
615
|
+
touched.set(false);
|
|
616
|
+
},
|
|
617
|
+
isValid: () => errors.val().length === 0
|
|
618
|
+
};
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Create email field
|
|
622
|
+
FormFields.email.setValue('test@example.com');
|
|
623
|
+
// Logs: "Creating field manager for: email"
|
|
624
|
+
|
|
625
|
+
FormFields.email.setErrors([]);
|
|
626
|
+
console.log(FormFields.email.isValid()); // true
|
|
627
|
+
|
|
628
|
+
// Create password field (different instance)
|
|
629
|
+
FormFields.password.setValue('weak');
|
|
630
|
+
// Logs: "Creating field manager for: password"
|
|
631
|
+
|
|
632
|
+
FormFields.password.setErrors(['Too short']);
|
|
633
|
+
console.log(FormFields.password.isValid()); // false
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Comparison: once vs singleton vs memoize
|
|
637
|
+
|
|
638
|
+
| Feature | `Cache.once()` | `Cache.singleton()` | `Cache.memoize()` |
|
|
639
|
+
|---------|------------------|------------------------|---------------------|
|
|
640
|
+
| **Execution** | Lazy (on property access) | Eager (on first call) | Lazy (per key on property access) |
|
|
641
|
+
| **Implementation** | `autoOnce` | `once` | `autoMemoize` |
|
|
642
|
+
| **Access Pattern** | `obj.property` | `fn()` | `obj[key].method()` |
|
|
643
|
+
| **Instances Created** | 1 (single instance) | 1 (single instance) | N (one per key) |
|
|
644
|
+
| **Cache Strategy** | Properties from result | Entire result | Result per property key |
|
|
645
|
+
| **Use Case** | Lazy modules | Eager singletons | Multiple instances by key |
|
|
646
|
+
|
|
647
|
+
### Visual Comparison
|
|
648
|
+
```javascript
|
|
649
|
+
import { Cache } from 'native-document/utils';
|
|
650
|
+
|
|
651
|
+
// Cache.once() - Single lazy instance
|
|
652
|
+
const LazyUtils = Cache.once(() => {
|
|
653
|
+
console.log('Init utils');
|
|
654
|
+
return {
|
|
655
|
+
format: () => 'formatted',
|
|
656
|
+
parse: () => 'parsed'
|
|
657
|
+
};
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
LazyUtils.format(); // Logs: "Init utils"
|
|
661
|
+
LazyUtils.parse(); // No log - same instance
|
|
662
|
+
|
|
663
|
+
// Cache.singleton() - Single eager instance
|
|
664
|
+
const getConfig = Cache.singleton(() => {
|
|
665
|
+
console.log('Init config');
|
|
666
|
+
return { api: 'url', debug: true };
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const config = getConfig(); // Logs: "Init config"
|
|
670
|
+
const config2 = getConfig(); // No log - same instance
|
|
671
|
+
|
|
672
|
+
// Cache.memoize() - Multiple instances by key
|
|
673
|
+
const API = Cache.memoize((resource) => {
|
|
674
|
+
console.log(`Init ${resource}`);
|
|
675
|
+
return {
|
|
676
|
+
list: () => `List ${resource}`,
|
|
677
|
+
get: (id) => `Get ${resource}/${id}`
|
|
678
|
+
};
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
API.users.list(); // Logs: "Init users"
|
|
682
|
+
API.users.get(1); // No log - cached 'users' instance
|
|
683
|
+
API.posts.list(); // Logs: "Init posts" - new key, new instance
|
|
684
|
+
API.posts.get(2); // No log - cached 'posts' instance
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
## When to Use Cache.memoize()
|
|
688
|
+
|
|
689
|
+
### ✅ Good Use Cases
|
|
690
|
+
```javascript
|
|
691
|
+
import { Cache } from 'native-document/utils';
|
|
692
|
+
|
|
693
|
+
// Multiple API endpoints with same structure
|
|
694
|
+
const API = Cache.memoize((resource) => ({
|
|
695
|
+
list: () => fetch(`/${resource}`),
|
|
696
|
+
get: (id) => fetch(`/${resource}/${id}`)
|
|
697
|
+
}));
|
|
698
|
+
|
|
699
|
+
// Multiple storage namespaces
|
|
700
|
+
const Storage = Cache.memoize((namespace) => ({
|
|
701
|
+
get: (key) => localStorage.getItem(`${namespace}:${key}`),
|
|
702
|
+
set: (key, val) => localStorage.setItem(`${namespace}:${key}`, val)
|
|
703
|
+
}));
|
|
704
|
+
|
|
705
|
+
// Multiple event types with same handler structure
|
|
706
|
+
const Events = Cache.memoize((type) => {
|
|
707
|
+
const listeners = [];
|
|
708
|
+
return {
|
|
709
|
+
on: (cb) => listeners.push(cb),
|
|
710
|
+
emit: (data) => listeners.forEach(cb => cb(data))
|
|
711
|
+
};
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// Multiple form fields with same structure
|
|
715
|
+
const Fields = Cache.memoize((name) => ({
|
|
716
|
+
value: Observable(''),
|
|
717
|
+
error: Observable(null),
|
|
718
|
+
validate: () => { /* ... */ }
|
|
719
|
+
}));
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### ❌ Bad Use Cases
|
|
723
|
+
```javascript
|
|
724
|
+
import { Cache } from 'native-document/utils';
|
|
725
|
+
|
|
726
|
+
// ❌ Don't use for single instance
|
|
727
|
+
const Config = Cache.memoize(() => loadConfig());
|
|
728
|
+
// Use Cache.singleton() instead
|
|
729
|
+
|
|
730
|
+
// ❌ Don't use when keys are unpredictable
|
|
731
|
+
const RandomData = Cache.memoize((timestamp) => generateData());
|
|
732
|
+
// Each call has unique timestamp - cache never hits
|
|
733
|
+
|
|
734
|
+
// ❌ Don't use for simple property access
|
|
735
|
+
const Constants = Cache.memoize(() => ({
|
|
736
|
+
PI: 3.14159,
|
|
737
|
+
E: 2.71828
|
|
738
|
+
}));
|
|
739
|
+
// Use Cache.once() instead
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
## Best Practices
|
|
743
|
+
|
|
744
|
+
### 1. Use Descriptive Keys
|
|
745
|
+
```javascript
|
|
746
|
+
import { Cache } from 'native-document/utils';
|
|
747
|
+
|
|
748
|
+
// ✅ Good: Clear, predictable keys
|
|
749
|
+
const Endpoints = Cache.memoize((resource) => createAPI(resource));
|
|
750
|
+
Endpoints.users.list();
|
|
751
|
+
Endpoints.posts.list();
|
|
752
|
+
|
|
753
|
+
// ❌ Bad: Dynamic, unpredictable keys
|
|
754
|
+
const DynamicAPI = Cache.memoize((timestamp) => createAPI(timestamp));
|
|
755
|
+
DynamicAPI[Date.now()].list(); // New instance every time
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### 2. Document Key-Based Behavior
|
|
759
|
+
```javascript
|
|
760
|
+
import { Cache } from 'native-document/utils';
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* API endpoints by resource type
|
|
764
|
+
* @memoized Each resource type gets its own instance
|
|
765
|
+
* @param {string} resource - Resource name (e.g., 'users', 'posts')
|
|
766
|
+
*/
|
|
767
|
+
const API = Cache.memoize((resource) => ({
|
|
768
|
+
list: () => fetch(`/${resource}`),
|
|
769
|
+
get: (id) => fetch(`/${resource}/${id}`)
|
|
770
|
+
}));
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### 3. Keep Keys Simple
|
|
774
|
+
```javascript
|
|
775
|
+
import { Cache } from 'native-document/utils';
|
|
776
|
+
|
|
777
|
+
// ✅ Good: Simple string keys
|
|
778
|
+
const Stores = Cache.memoize((name) => Store.create(name, {}));
|
|
779
|
+
Stores.user;
|
|
780
|
+
Stores.settings;
|
|
781
|
+
|
|
782
|
+
// ❌ Bad: Complex keys (won't work as expected)
|
|
783
|
+
const BadCache = Cache.memoize((config) => createThing(config));
|
|
784
|
+
BadCache[{ type: 'user' }]; // Object as key - problematic
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
### 4. Combine with Other Patterns
|
|
788
|
+
```javascript
|
|
789
|
+
import { Cache } from 'native-document/utils';
|
|
790
|
+
|
|
791
|
+
// Eager config + Memoized resources
|
|
792
|
+
const getConfig = Cache.singleton(() => loadConfig());
|
|
793
|
+
|
|
794
|
+
const Resources = Cache.memoize((type) => {
|
|
795
|
+
const config = getConfig(); // Config already loaded
|
|
796
|
+
return createResource(type, config);
|
|
797
|
+
});
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
## Performance Considerations
|
|
801
|
+
|
|
802
|
+
### Memory Usage
|
|
803
|
+
```javascript
|
|
804
|
+
import { Cache } from 'native-document/utils';
|
|
805
|
+
|
|
806
|
+
// ⚠️ Each key creates a new cached instance
|
|
807
|
+
const API = Cache.memoize((resource) => createAPI(resource));
|
|
808
|
+
|
|
809
|
+
API.users; // Instance 1
|
|
810
|
+
API.posts; // Instance 2
|
|
811
|
+
API.comments; // Instance 3
|
|
812
|
+
// All instances stay in memory
|
|
813
|
+
|
|
814
|
+
// Consider: Do you need separate instances per key?
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Cache Growth
|
|
818
|
+
```javascript
|
|
819
|
+
import { Cache } from 'native-document/utils';
|
|
820
|
+
|
|
821
|
+
// ⚠️ Warning: Unbounded cache growth
|
|
822
|
+
const DynamicCache = Cache.memoize((id) => createInstance(id));
|
|
823
|
+
|
|
824
|
+
// If IDs keep changing, cache grows indefinitely
|
|
825
|
+
for (let i = 0; i < 1000; i++) {
|
|
826
|
+
DynamicCache[`user-${i}`]; // 1000 cached instances
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ✅ Better: Limited, predictable keys
|
|
830
|
+
const ResourceCache = Cache.memoize((type) => createResource(type));
|
|
831
|
+
ResourceCache.users; // Only a few known types
|
|
832
|
+
ResourceCache.posts;
|
|
833
|
+
ResourceCache.comments;
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
## Summary
|
|
837
|
+
|
|
838
|
+
### Quick Reference
|
|
839
|
+
|
|
840
|
+
| Pattern | Syntax | When to Use |
|
|
841
|
+
|---------|--------|-------------|
|
|
842
|
+
| **Lazy Module** | `Cache.once(() => {...})` | Optional features, heavy resources |
|
|
843
|
+
| **Eager Singleton** | `Cache.singleton(() => {...})` | Core services, configuration |
|
|
844
|
+
| **Multi-Instance** | `Cache.memoize((key) => {...})` | Resources by type, namespaced data |
|
|
845
|
+
|
|
846
|
+
### Key Differences
|
|
847
|
+
```javascript
|
|
848
|
+
import { Cache } from 'native-document/utils';
|
|
849
|
+
|
|
850
|
+
// Cache.once() - Single lazy instance via autoOnce
|
|
851
|
+
const Lazy = Cache.once(() => ({ value: 1 }));
|
|
852
|
+
Lazy.value; // Creates instance on property access
|
|
853
|
+
|
|
854
|
+
// Cache.singleton() - Single eager instance via once
|
|
855
|
+
const Eager = Cache.singleton(() => ({ value: 1 }));
|
|
856
|
+
Eager(); // Creates instance on function call
|
|
857
|
+
|
|
858
|
+
// Cache.memoize() - Multiple instances by key via autoMemoize
|
|
859
|
+
const Multi = Cache.memoize((key) => ({ value: key }));
|
|
860
|
+
Multi.a; // Creates instance for key 'a'
|
|
861
|
+
Multi.b; // Creates instance for key 'b'
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
## Next Steps
|
|
865
|
+
|
|
866
|
+
Explore related utilities and concepts:
|
|
867
|
+
|
|
868
|
+
## Next Steps
|
|
869
|
+
|
|
870
|
+
- **[Getting Started](getting-started.md)** - Installation and first steps
|
|
871
|
+
- **[Core Concepts](core-concepts.md)** - Understanding the fundamentals
|
|
872
|
+
- **[Observables](observables.md)** - Reactive state management
|
|
873
|
+
- **[Elements](elements.md)** - Creating and composing UI
|
|
874
|
+
- **[Conditional Rendering](conditional-rendering.md)** - Dynamic content
|
|
875
|
+
- **[List Rendering](list-rendering.md)** - (ForEach | ForEachArray) and dynamic lists
|
|
876
|
+
- **[Routing](routing.md)** - Navigation and URL management
|
|
877
|
+
- **[State Management](state-management.md)** - Global state patterns
|
|
878
|
+
- **[NDElement](native-document-element.md)** - Native Document Element
|
|
879
|
+
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
880
|
+
- **[Advanced Components](advanced-components.md)** - Template caching and singleton views
|
|
881
|
+
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
882
|
+
- **[Memory Management](memory-management.md)** - Memory management
|
|
883
|
+
|
|
884
|
+
## Utilities
|
|
885
|
+
|
|
886
|
+
- **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
|
|
887
|
+
- **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
|
|
888
|
+
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|