native-document 1.0.166 → 1.0.168
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/.vitepress/config.js +166 -0
- package/CHANGELOG.md +153 -0
- package/components.js +2 -1
- package/dist/native-document.components.min.js +495 -228
- package/dist/native-document.dev.js +7 -0
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/advanced-components.md +213 -608
- package/docs/anchor.md +173 -312
- package/docs/cache.md +95 -803
- package/docs/cli.md +179 -0
- package/docs/components/accordion.md +172 -0
- package/docs/components/alert.md +99 -0
- package/docs/components/avatar.md +160 -0
- package/docs/components/badge.md +102 -0
- package/docs/components/breadcrumb.md +89 -0
- package/docs/components/button.md +183 -0
- package/docs/components/card.md +69 -0
- package/docs/components/context-menu.md +118 -0
- package/docs/components/data-table.md +345 -0
- package/docs/components/dropdown.md +214 -0
- package/docs/components/form/autocomplete-field.md +81 -0
- package/docs/components/form/checkbox-field.md +41 -0
- package/docs/components/form/checkbox-group-field.md +54 -0
- package/docs/components/form/color-field.md +64 -0
- package/docs/components/form/date-field.md +92 -0
- package/docs/components/form/field-collection.md +63 -0
- package/docs/components/form/file-field.md +203 -0
- package/docs/components/form/form-control.md +87 -0
- package/docs/components/form/image-field.md +90 -0
- package/docs/components/form/index.md +115 -0
- package/docs/components/form/number-field.md +65 -0
- package/docs/components/form/radio-field.md +51 -0
- package/docs/components/form/select-field.md +123 -0
- package/docs/components/form/slider.md +136 -0
- package/docs/components/form/string-field.md +134 -0
- package/docs/components/form/textarea-field.md +65 -0
- package/docs/components/form-fields.md +372 -0
- package/docs/components/getting-started.md +264 -0
- package/docs/components/index.md +337 -0
- package/docs/components/layout.md +279 -0
- package/docs/components/list.md +73 -0
- package/docs/components/menu.md +215 -0
- package/docs/components/modal.md +156 -0
- package/docs/components/pagination.md +95 -0
- package/docs/components/popover.md +131 -0
- package/docs/components/progress.md +111 -0
- package/docs/components/shortcut-manager.md +221 -0
- package/docs/components/simple-table.md +107 -0
- package/docs/components/skeleton.md +155 -0
- package/docs/components/spinner.md +100 -0
- package/docs/components/splitter.md +133 -0
- package/docs/components/stepper.md +163 -0
- package/docs/components/switch.md +113 -0
- package/docs/components/tabs.md +153 -0
- package/docs/components/toast.md +119 -0
- package/docs/components/tooltip.md +151 -0
- package/docs/components/traits.md +261 -0
- package/docs/conditional-rendering.md +170 -588
- package/docs/contributing.md +300 -25
- package/docs/core-concepts.md +205 -374
- package/docs/elements.md +251 -367
- package/docs/extending-native-document-element.md +192 -207
- package/docs/filters.md +153 -1122
- package/docs/getting-started.md +193 -267
- package/docs/i18n.md +241 -0
- package/docs/index.md +76 -0
- package/docs/lifecycle-events.md +143 -75
- package/docs/list-rendering.md +227 -852
- package/docs/memory-management.md +134 -47
- package/docs/native-document-element.md +337 -186
- package/docs/native-fetch.md +99 -630
- package/docs/observable-resource.md +364 -0
- package/docs/observables.md +592 -526
- package/docs/routing.md +244 -653
- package/docs/state-management.md +134 -241
- package/docs/svg-elements.md +231 -0
- package/docs/theming.md +409 -0
- package/docs/tutorials/.gitkeep +0 -0
- package/docs/validation.md +95 -97
- package/docs/vitepress-conventions.md +219 -0
- package/package.json +34 -13
- package/readme.md +269 -89
- package/src/components/card/Card.js +93 -39
- package/src/components/card/index.js +1 -1
- package/src/components/list/HasListItem.js +171 -0
- package/src/components/list/List.js +41 -107
- package/src/components/list/ListDivider.js +39 -0
- package/src/components/list/ListGroup.js +76 -59
- package/src/components/list/ListItem.js +117 -69
- package/src/components/list/index.js +3 -1
- package/src/components/list/types/ListItem.d.ts +45 -34
- package/src/components/spacer/Spacer.js +1 -1
- package/src/core/data/ObservableResource.js +5 -0
- package/src/core/data/observable-helpers/observable.prototypes.js +2 -0
- package/src/ui/components/card/CardRender.js +133 -0
- package/src/ui/components/card/card.css +169 -0
- package/src/ui/components/contextmenu/ContextmenuRender.js +1 -1
- package/src/ui/components/list/ListRender.js +18 -0
- package/src/ui/components/list/divider/ListDividerRender.js +10 -0
- package/src/ui/components/list/divider/list-divider.css +12 -0
- package/src/ui/components/list/group/ListGroupRender.js +61 -0
- package/src/ui/components/list/group/list-group.css +62 -0
- package/src/ui/components/list/item/ListItemRender.js +238 -0
- package/src/ui/components/list/item/list-item.css +191 -0
- package/src/ui/components/list/list.css +24 -0
- package/src/ui/components/spacer/SpacerRender.js +10 -0
- package/src/ui/index.js +8 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Observable Resource
|
|
3
|
+
description: Manage async data fetching with built-in states, AbortController cancellation, reactive dependencies, and debouncing
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Observable Resource
|
|
7
|
+
|
|
8
|
+
`Observable.resource()` manages async data fetching with built-in lifecycle states, automatic AbortController cancellation, reactive dependency tracking, and debounce support.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
import { Observable } from 'native-document';
|
|
16
|
+
|
|
17
|
+
const user = Observable.resource(
|
|
18
|
+
async () => {
|
|
19
|
+
const res = await fetch('/api/user');
|
|
20
|
+
return res.json();
|
|
21
|
+
},
|
|
22
|
+
[],
|
|
23
|
+
{ auto: true }
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Signature
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
Observable.resource(fn, deps, options)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Parameter | Type | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `fn` | `async function` | Async function that returns the data. Receives dependency values as arguments. If the function declares more parameters than there are dependencies, the last parameter receives an `AbortSignal` |
|
|
38
|
+
| `deps` | `Observable[]` | Reactive dependencies - resource re-fetches when any of these change |
|
|
39
|
+
| `options` | `object` | Configuration object |
|
|
40
|
+
|
|
41
|
+
### Options
|
|
42
|
+
|
|
43
|
+
| Option | Type | Default | Description |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `auto` | `boolean` | `false` | Fetch immediately on creation, and re-fetch when deps change |
|
|
46
|
+
| `lazy` | `boolean` | `false` | With deps, skip the initial fetch - only re-fetch when deps change |
|
|
47
|
+
| `debounce` | `number` | `0` | Debounce re-fetches by N milliseconds |
|
|
48
|
+
| `into` | `Observable` | `Observable(null)` | Observable to use as `data`. Pass `$.array()` for list results, or an existing observable to reuse |
|
|
49
|
+
| `apply` | `function` | `null` | Custom function to apply the fetch result to `data`: `(result, data) => void` |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Resource Properties
|
|
54
|
+
|
|
55
|
+
| Property | Type | Description |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `data` | `Observable` | The fetched data. Starts as `null` unless `into` is provided |
|
|
58
|
+
| `error` | `Observable` | The error if the last fetch failed, otherwise `null` |
|
|
59
|
+
| `state` | `Observable<string>` | Current state |
|
|
60
|
+
| `loading` | `Observable<boolean>` | `true` when state is `'pending'` or `'refreshing'` |
|
|
61
|
+
|
|
62
|
+
### States
|
|
63
|
+
|
|
64
|
+
| State | When |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `unresolved` | Created but not yet fetched |
|
|
67
|
+
| `pending` | First fetch in progress, no data yet |
|
|
68
|
+
| `ready` | Data available |
|
|
69
|
+
| `refreshing` | Re-fetching while previous data is still shown |
|
|
70
|
+
| `errored` | Last fetch failed |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## State Checkers
|
|
75
|
+
|
|
76
|
+
All return an `ObservableChecker<boolean>`:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
user.isUnresolved()
|
|
80
|
+
user.isPending()
|
|
81
|
+
user.isReady()
|
|
82
|
+
user.isRefreshing()
|
|
83
|
+
user.isErrored()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Methods
|
|
89
|
+
|
|
90
|
+
### `fetch()` - Trigger first fetch
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
const user = Observable.resource(fetchUser, []);
|
|
94
|
+
user.fetch();
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `refetch()` - Re-fetch (shows `'refreshing'` state)
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
Button('Refresh').nd.onClick(() => user.refetch())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `mutate(value)` - Update data optimistically
|
|
104
|
+
|
|
105
|
+
Updates `data` directly and sets state to `'ready'` without a network request:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
user.mutate({ ...user.data.val(), name: 'Alice Updated' });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `into($observable)` - Set or change the data observable
|
|
112
|
+
|
|
113
|
+
Can be passed as an option or as a chained method after creation.
|
|
114
|
+
|
|
115
|
+
> **Warning:** When `auto: true`, the fetch starts immediately in the constructor - before any chained method calls. If you call `.into()` or `.apply()` as chained methods, the first fetch may already be in progress and will use the default `data` observable, causing an async conflict. **Always pass `into` and `apply` as options when using `auto: true`.**
|
|
116
|
+
>
|
|
117
|
+
> When `auto: false`, chaining is safe - the fetch has not started yet:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
// Safe - auto: false, chain .into() then trigger manually
|
|
121
|
+
Observable.resource(fetchUsers, [])
|
|
122
|
+
.into($.array())
|
|
123
|
+
.fetch();
|
|
124
|
+
|
|
125
|
+
// Safe - into passed as option with auto: true
|
|
126
|
+
const users = Observable.resource(fetchUsers, [], {
|
|
127
|
+
auto: true,
|
|
128
|
+
into: $.array()
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Unsafe - fetch may start before .into() is called
|
|
132
|
+
const users = Observable.resource(fetchUsers, [], { auto: true })
|
|
133
|
+
.into($.array()); // race condition!
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `apply(fn)` - Custom result application
|
|
137
|
+
|
|
138
|
+
By default the result is applied via `data.set(result)`. Use `apply` when you need custom merge logic.
|
|
139
|
+
|
|
140
|
+
> **Warning:** Same async conflict as `into()` - always pass `apply` as an option when `auto: true`. When `auto: false`, chaining is safe:
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
// Safe - auto: false, chain .apply() then trigger manually
|
|
144
|
+
Observable.resource(fetchUsers, [])
|
|
145
|
+
.into($.array())
|
|
146
|
+
.apply((result, data) => data.merge(result))
|
|
147
|
+
.fetch();
|
|
148
|
+
|
|
149
|
+
// Safe - apply passed as option with auto: true
|
|
150
|
+
const users = Observable.resource(fetchUsers, [], {
|
|
151
|
+
auto: true,
|
|
152
|
+
into: $.array(),
|
|
153
|
+
apply: (result, data) => data.merge(result)
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Unsafe - apply may not be set before first fetch completes
|
|
157
|
+
const users = Observable.resource(fetchUsers, [], { auto: true })
|
|
158
|
+
.apply((result, data) => data.merge(result)); // race condition!
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `onSuccess(callback)`
|
|
162
|
+
|
|
163
|
+
Fires when a fetch completes successfully:
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
user.onSuccess(data => console.log('Loaded:', data));
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `onError(callback)`
|
|
170
|
+
|
|
171
|
+
Fires when a fetch fails:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
user.onError(err => console.error('Failed:', err.message));
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### `destroy()` - Cleanup
|
|
178
|
+
|
|
179
|
+
Aborts any in-flight request and unsubscribes from all dependencies:
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
user.destroy();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Rendering Patterns
|
|
188
|
+
|
|
189
|
+
### With `Match`
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
import { Match, Div } from 'native-document/elements';
|
|
193
|
+
|
|
194
|
+
const user = Observable.resource(
|
|
195
|
+
async () => {
|
|
196
|
+
const res = await fetch('/api/user');
|
|
197
|
+
return res.json();
|
|
198
|
+
},
|
|
199
|
+
[],
|
|
200
|
+
{ auto: true }
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
Match(user.state, {
|
|
204
|
+
unresolved: Div('Not started'),
|
|
205
|
+
pending: Div('Loading...'),
|
|
206
|
+
refreshing: () => Div([
|
|
207
|
+
Div({ class: 'opacity-50' }, user.data.select(u => u.name)),
|
|
208
|
+
Div('Refreshing...')
|
|
209
|
+
]),
|
|
210
|
+
ready: () => Div(user.data.select(u => u.name)),
|
|
211
|
+
errored: () => Div(['Error: ', user.error.select(e => e.message)])
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### With `ShowIf`
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
Div([
|
|
219
|
+
ShowIf(user.loading, () => Div('Loading...')),
|
|
220
|
+
ShowIf(user.isReady(), () => P(user.data.select(u => u.name))),
|
|
221
|
+
ShowIf(user.isErrored(), () => Div(['Error: ', user.error.select(e => e.message)])),
|
|
222
|
+
Button('Refresh').nd.onClick(() => user.refetch())
|
|
223
|
+
])
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Reactive Dependencies
|
|
229
|
+
|
|
230
|
+
When deps change, the resource re-fetches. Dependency values are passed as arguments to `fn` in order:
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
const userId = Observable(1);
|
|
234
|
+
const language = Observable('en');
|
|
235
|
+
|
|
236
|
+
const profile = Observable.resource(
|
|
237
|
+
async (id, lang) => {
|
|
238
|
+
const res = await fetch(`/api/users/${id}?lang=${lang}`);
|
|
239
|
+
return res.json();
|
|
240
|
+
},
|
|
241
|
+
[userId, language],
|
|
242
|
+
{ auto: true }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Triggers a re-fetch automatically
|
|
246
|
+
userId.set(2);
|
|
247
|
+
language.set('fr');
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## AbortController Support
|
|
253
|
+
|
|
254
|
+
If `fn` declares **more parameters than there are dependencies**, the last parameter receives an `AbortSignal`. Previous requests are aborted automatically when a new fetch starts:
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
const userId = Observable(1);
|
|
258
|
+
|
|
259
|
+
const user = Observable.resource(
|
|
260
|
+
async (id, signal) => {
|
|
261
|
+
// fn.length (2) > deps.length (1) -> AbortController enabled
|
|
262
|
+
const res = await fetch(`/api/users/${id}`, { signal });
|
|
263
|
+
return res.json();
|
|
264
|
+
},
|
|
265
|
+
[userId],
|
|
266
|
+
{ auto: true }
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Rapid changes abort in-flight requests
|
|
270
|
+
userId.set(2);
|
|
271
|
+
userId.set(3); // request for id=2 is aborted
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
If `fn.length === deps.length`, no AbortController is used.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Debouncing
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
const search = Observable('');
|
|
282
|
+
|
|
283
|
+
const results = Observable.resource(
|
|
284
|
+
async (query) => {
|
|
285
|
+
const res = await fetch(`/api/search?q=${query}`);
|
|
286
|
+
return res.json();
|
|
287
|
+
},
|
|
288
|
+
[search],
|
|
289
|
+
{ auto: true, debounce: 300 }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
Input({ value: search, placeholder: 'Search...' })
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Lazy Loading
|
|
298
|
+
|
|
299
|
+
With `lazy: true` and deps, the initial fetch is skipped. The resource only fetches when a dependency changes:
|
|
300
|
+
|
|
301
|
+
```javascript
|
|
302
|
+
const isOpen = Observable(false);
|
|
303
|
+
|
|
304
|
+
const data = Observable.resource(
|
|
305
|
+
async (open) => {
|
|
306
|
+
if (!open) return null;
|
|
307
|
+
const res = await fetch('/api/data');
|
|
308
|
+
return res.json();
|
|
309
|
+
},
|
|
310
|
+
[isOpen],
|
|
311
|
+
{ auto: true, lazy: true } // no fetch on creation
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// First fetch triggered when isOpen changes
|
|
315
|
+
isOpen.set(true);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## `into` - Control the Data Observable
|
|
321
|
+
|
|
322
|
+
Use `into` as an **option** to control the type of `data` or reuse an existing observable. Always pass it as an option when `auto: true` - not as a chained method call:
|
|
323
|
+
|
|
324
|
+
```javascript
|
|
325
|
+
import { $ } from 'native-document';
|
|
326
|
+
|
|
327
|
+
// Default - plain Observable(null)
|
|
328
|
+
const user = Observable.resource(fetchUser, [], { auto: true });
|
|
329
|
+
|
|
330
|
+
// ObservableArray - useful for list results
|
|
331
|
+
const users = Observable.resource(fetchUsers, [], {
|
|
332
|
+
auto: true,
|
|
333
|
+
into: $.array()
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Existing observable - e.g. a cached Store value shown immediately
|
|
337
|
+
const cachedUser = Store.follow('user');
|
|
338
|
+
const user = Observable.resource(fetchUser, [], {
|
|
339
|
+
auto: true,
|
|
340
|
+
into: cachedUser
|
|
341
|
+
});
|
|
342
|
+
// user.data === cachedUser, shown immediately, updated when fetch completes
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## vs `Observable.batch()`
|
|
348
|
+
|
|
349
|
+
| | `Observable.resource()` | `Observable.batch()` |
|
|
350
|
+
|---|---|---|
|
|
351
|
+
| **Purpose** | Async data fetching | Batching multiple state changes |
|
|
352
|
+
| **States** | Built-in (pending, ready, errored...) | None |
|
|
353
|
+
| **AbortController** | Automatic | Manual |
|
|
354
|
+
| **Deps tracking** | Auto re-fetch | Manual call |
|
|
355
|
+
| **Best for** | API calls, remote data | Complex local state updates |
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Next Steps
|
|
360
|
+
|
|
361
|
+
- **[Observables](./observables.md)** - Reactive state management
|
|
362
|
+
- **[NativeFetch](./native-fetch.md)** - HTTP client with interceptors
|
|
363
|
+
- **[State Management](./state-management.md)** - Global state with Store
|
|
364
|
+
- **[Memory Management](./memory-management.md)** - Cleanup with `destroy()`
|