@wcstack/fetch 1.4.0

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/README.md ADDED
@@ -0,0 +1,625 @@
1
+ # @wcstack/fetch
2
+
3
+ `@wcstack/fetch` is a headless fetch component for the wcstack ecosystem.
4
+
5
+ It is not a visual UI widget.
6
+ It is an **I/O node** that connects HTTP requests to reactive state.
7
+
8
+ With `@wcstack/state`, `<wcs-fetch>` can be bound directly through path contracts:
9
+
10
+ - **input / command surface**: `url`, `body`, `trigger`
11
+ - **output state surface**: `value`, `loading`, `error`, `status`
12
+
13
+ This means async communication can be expressed declaratively in HTML, without writing `fetch()`, `async/await`, or loading/error glue code in your UI layer.
14
+
15
+ `@wcstack/fetch` follows the [HAWC](https://github.com/wc-bindable-protocol/wc-bindable-protocol/blob/main/docs/articles/HAWC.md) architecture:
16
+
17
+ - **Core** (`FetchCore`) handles HTTP, abort, and async state
18
+ - **Shell** (`<wcs-fetch>`) connects that state to the DOM
19
+ - frameworks and binding systems consume it through [wc-bindable-protocol](https://github.com/wc-bindable-protocol/wc-bindable-protocol)
20
+
21
+ ## Why this exists
22
+
23
+ In many frontend apps, the hardest part to migrate is not the template — it is the async logic:
24
+ HTTP requests, loading flags, errors, retries, and lifecycle cleanup.
25
+
26
+ `@wcstack/fetch` moves that async logic into a reusable component and exposes the result as bindable state.
27
+
28
+ With `@wcstack/state`, the flow becomes:
29
+
30
+ 1. state computes `url`
31
+ 2. `<wcs-fetch>` executes the request
32
+ 3. async results return as `value`, `loading`, `error`, `status`
33
+ 4. UI binds to those paths with `data-wcs`
34
+
35
+ This turns async communication into **state transitions**, not imperative UI code.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ npm install @wcstack/fetch
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ### 1. Reactive fetch from state
46
+
47
+ When `url` changes, `<wcs-fetch>` automatically runs a new request.
48
+ If another request is already in flight, it aborts the previous one.
49
+
50
+ ```html
51
+ <script type="module" src="https://esm.run/@wcstack/state/auto"></script>
52
+ <script type="module" src="https://esm.run/@wcstack/fetch/auto"></script>
53
+
54
+ <wcs-state>
55
+ <script type="module">
56
+ export default {
57
+ users: [],
58
+ get usersUrl() {
59
+ return "/api/users";
60
+ },
61
+ };
62
+ </script>
63
+
64
+ <wcs-fetch data-wcs="url: usersUrl; value: users"></wcs-fetch>
65
+
66
+ <ul>
67
+ <template data-wcs="for: users">
68
+ <li data-wcs="textContent: users.*.name"></li>
69
+ </template>
70
+ </ul>
71
+ </wcs-state>
72
+ ```
73
+
74
+ This is the default mode:
75
+
76
+ - connect `url`
77
+ - receive `value`
78
+ - optionally bind `loading`, `error`, and `status`
79
+
80
+ ### 2. Reactive URL example
81
+
82
+ A computed URL can drive data fetching automatically:
83
+
84
+ ```html
85
+ <wcs-state>
86
+ <script type="module">
87
+ export default {
88
+ filterRole: "",
89
+ users: [],
90
+
91
+ get usersUrl() {
92
+ const role = this.filterRole;
93
+ return role ? "/api/users?role=" + role : "/api/users";
94
+ },
95
+ };
96
+ </script>
97
+
98
+ <select data-wcs="value: filterRole">
99
+ <option value="">All</option>
100
+ <option value="admin">Admin</option>
101
+ <option value="staff">Staff</option>
102
+ </select>
103
+
104
+ <wcs-fetch
105
+ data-wcs="url: usersUrl; value: users; loading: listLoading; error: listError">
106
+ </wcs-fetch>
107
+
108
+ <template data-wcs="if: listLoading">
109
+ <p>Loading...</p>
110
+ </template>
111
+ <template data-wcs="if: listError">
112
+ <p>Failed to load users.</p>
113
+ </template>
114
+
115
+ <ul>
116
+ <template data-wcs="for: users">
117
+ <li data-wcs="textContent: users.*.name"></li>
118
+ </template>
119
+ </ul>
120
+ </wcs-state>
121
+ ```
122
+
123
+ ### 3. Manual execution with `trigger`
124
+
125
+ Use `manual` when you want to prepare inputs first and execute later.
126
+
127
+ ```html
128
+ <wcs-state>
129
+ <script type="module">
130
+ export default {
131
+ users: [],
132
+ shouldRefresh: false,
133
+
134
+ reload() {
135
+ this.shouldRefresh = true;
136
+ },
137
+ };
138
+ </script>
139
+
140
+ <wcs-fetch
141
+ url="/api/users"
142
+ manual
143
+ data-wcs="trigger: shouldRefresh; value: users; loading: listLoading">
144
+ </wcs-fetch>
145
+
146
+ <button data-wcs="onclick: reload">Refresh</button>
147
+ </wcs-state>
148
+ ```
149
+
150
+ `trigger` is a **one-way command surface**:
151
+
152
+ - writing `true` starts `fetch()`
153
+ - it resets itself to `false` after completion
154
+ - the reset emits `wcs-fetch:trigger-changed`
155
+
156
+ ```
157
+ external write: false → true No event (triggers fetch)
158
+ auto-reset: true → false Dispatches wcs-fetch:trigger-changed
159
+ ```
160
+
161
+ ### 4. POST with reactive body
162
+
163
+ ```html
164
+ <wcs-state>
165
+ <script type="module">
166
+ export default {
167
+ newUser: {
168
+ name: "",
169
+ email: "",
170
+ },
171
+ submitRequest: false,
172
+ submitResult: null,
173
+ submitError: null,
174
+
175
+ submit() {
176
+ this.submitRequest = true;
177
+ },
178
+ };
179
+ </script>
180
+
181
+ <input data-wcs="value: newUser.name" placeholder="Name">
182
+ <input data-wcs="value: newUser.email" placeholder="Email">
183
+
184
+ <button data-wcs="onclick: submit">Create</button>
185
+
186
+ <wcs-fetch
187
+ url="/api/users"
188
+ method="POST"
189
+ manual
190
+ data-wcs="
191
+ body: newUser;
192
+ trigger: submitRequest;
193
+ value: submitResult;
194
+ error: submitError;
195
+ loading: submitLoading
196
+ ">
197
+ <wcs-fetch-header name="Content-Type" value="application/json"></wcs-fetch-header>
198
+ </wcs-fetch>
199
+
200
+ <template data-wcs="if: submitLoading">
201
+ <p>Submitting...</p>
202
+ </template>
203
+ <template data-wcs="if: submitError">
204
+ <p>Submit failed.</p>
205
+ </template>
206
+ </wcs-state>
207
+ ```
208
+
209
+ ## State Surface vs Command Surface
210
+
211
+ `<wcs-fetch>` exposes two different kinds of properties.
212
+
213
+ ### Output state (bindable async state)
214
+
215
+ These properties represent the result of the current request and are the main HAWC surface:
216
+
217
+ | Property | Type | Description |
218
+ |----------|------|-------------|
219
+ | `value` | `any` | Response data |
220
+ | `loading` | `boolean` | `true` while a request is in flight |
221
+ | `error` | `WcsFetchHttpError \| Error \| null` | HTTP or network error |
222
+ | `status` | `number` | HTTP status code |
223
+
224
+ ### Input / command surface
225
+
226
+ These properties control request execution from HTML, JS, or `@wcstack/state` bindings:
227
+
228
+ | Property | Type | Description |
229
+ |----------|------|-------------|
230
+ | `url` | `string` | Request URL |
231
+ | `body` | `any` | Request body (resets to `null` after `fetch()`) |
232
+ | `trigger` | `boolean` | One-way execution trigger |
233
+ | `manual` | `boolean` | Disables auto-fetch on connect / URL change |
234
+
235
+ ## Architecture
236
+
237
+ `@wcstack/fetch` follows the HAWC architecture.
238
+
239
+ ### Core: `FetchCore`
240
+
241
+ `FetchCore` is a pure `EventTarget` class.
242
+ It contains:
243
+
244
+ - HTTP execution
245
+ - abort control
246
+ - async state transitions
247
+ - `wc-bindable-protocol` declaration
248
+
249
+ It can run headlessly in any runtime that supports `EventTarget` and `fetch`.
250
+
251
+ ### Shell: `<wcs-fetch>`
252
+
253
+ `<wcs-fetch>` is a thin `HTMLElement` wrapper around `FetchCore`.
254
+ It adds:
255
+
256
+ - attribute / property mapping
257
+ - DOM lifecycle integration
258
+ - declarative execution helpers such as `trigger`
259
+
260
+ This split keeps the async logic portable while allowing DOM-based binding systems such as `@wcstack/state` to interact with it naturally.
261
+
262
+ ### Target injection
263
+
264
+ The Core dispatches events directly on the Shell via **target injection**, so no event re-dispatch is needed.
265
+
266
+ ## Headless Usage (Core only)
267
+
268
+ `FetchCore` can be used standalone without the DOM. Since it declares `static wcBindable`, you can use `@wc-bindable/core`'s `bind()` to subscribe to its state — the same way framework adapters work:
269
+
270
+ ```typescript
271
+ import { FetchCore } from "@wcstack/fetch";
272
+ import { bind } from "@wc-bindable/core";
273
+
274
+ const core = new FetchCore();
275
+
276
+ const unbind = bind(core, (name, value) => {
277
+ console.log(`${name}:`, value);
278
+ });
279
+
280
+ await core.fetch("/api/users");
281
+
282
+ unbind();
283
+ ```
284
+
285
+ This works in Node.js, Deno, Cloudflare Workers — anywhere `EventTarget` and `fetch` are available.
286
+
287
+ ## URL Observation
288
+
289
+ By default, `<wcs-fetch>` automatically executes a request when:
290
+
291
+ 1. it is connected to the DOM and `url` is set
292
+ 2. the `url` changes
293
+
294
+ If a request is already in flight when the URL changes, the previous request is automatically aborted before the new one starts.
295
+
296
+ Set the `manual` attribute to disable auto-fetch and control execution explicitly via `fetch()` or `trigger`.
297
+
298
+ ## Programmatic Usage
299
+
300
+ ```javascript
301
+ const fetchEl = document.querySelector("wcs-fetch");
302
+
303
+ // Set body via JS API (takes priority over <wcs-fetch-body>)
304
+ fetchEl.body = { name: "Tanaka" };
305
+ await fetchEl.fetch();
306
+ // Note: body is automatically reset to null after fetch().
307
+ // Set it again before each call if needed.
308
+
309
+ console.log(fetchEl.value); // response data
310
+ console.log(fetchEl.status); // HTTP status code
311
+ console.log(fetchEl.loading); // boolean
312
+ console.log(fetchEl.error); // error info or null
313
+ console.log(fetchEl.body); // null (reset after fetch)
314
+ ```
315
+
316
+ ## HTML Replace Mode
317
+
318
+ `<wcs-fetch>` can also replace a target element's `innerHTML` when `target` is set.
319
+
320
+ ```html
321
+ <div id="content">Initial content</div>
322
+ <wcs-fetch url="/api/partial" target="content"></wcs-fetch>
323
+ ```
324
+
325
+ This mode is useful for simple fragment loading, but it is separate from the main **state-driven** usage with `@wcstack/state`.
326
+
327
+ ## Optional DOM Triggering
328
+
329
+ If `autoTrigger` is enabled (default), clicking an element with `data-fetchtarget` triggers the corresponding `<wcs-fetch>` element:
330
+
331
+ ```html
332
+ <button data-fetchtarget="user-fetch">Load Users</button>
333
+ <wcs-fetch id="user-fetch" url="/api/users"></wcs-fetch>
334
+ ```
335
+
336
+ Event delegation is used — works with dynamically added elements. The `closest()` API handles nested elements (e.g., icon inside a button).
337
+
338
+ If the target id does not match any element, or the matched element is not a `<wcs-fetch>`, the click is silently ignored.
339
+
340
+ This is a convenience feature.
341
+ In wcstack applications, **state-driven triggering via `trigger`** is usually the primary pattern.
342
+
343
+ ## Elements
344
+
345
+ ### `<wcs-fetch>`
346
+
347
+ | Attribute | Type | Default | Description |
348
+ |-----------|------|---------|-------------|
349
+ | `url` | `string` | — | Request URL |
350
+ | `method` | `string` | `GET` | HTTP method |
351
+ | `target` | `string` | — | DOM element id for HTML replace mode |
352
+ | `manual` | `boolean` | `false` | Disable auto-fetch |
353
+
354
+ | Property | Type | Description |
355
+ |----------|------|-------------|
356
+ | `value` | `any` | Response data |
357
+ | `loading` | `boolean` | `true` while request is in flight |
358
+ | `error` | `WcsFetchHttpError \| Error \| null` | Error info |
359
+ | `status` | `number` | HTTP status code |
360
+ | `body` | `any` | Request body (resets to `null` after `fetch()`) |
361
+ | `trigger` | `boolean` | Set to `true` to execute fetch |
362
+ | `manual` | `boolean` | Explicit execution mode |
363
+
364
+ | Method | Description |
365
+ |--------|-------------|
366
+ | `fetch()` | Execute the HTTP request |
367
+ | `abort()` | Cancel the in-flight request |
368
+
369
+ ### `<wcs-fetch-header>`
370
+
371
+ Defines a request header. Place it as a child of `<wcs-fetch>`.
372
+
373
+ | Attribute | Type | Description |
374
+ |-----------|------|-------------|
375
+ | `name` | `string` | Header name |
376
+ | `value` | `string` | Header value |
377
+
378
+ ### `<wcs-fetch-body>`
379
+
380
+ Defines the request body. Place it as a child of `<wcs-fetch>`.
381
+
382
+ | Attribute | Type | Default | Description |
383
+ |-----------|------|---------|-------------|
384
+ | `type` | `string` | `application/json` | Content-Type |
385
+
386
+ The body content is taken from the element's text content.
387
+
388
+ Example:
389
+
390
+ ```html
391
+ <wcs-fetch url="/api/users" method="POST">
392
+ <wcs-fetch-header name="Authorization" value="Bearer token123"></wcs-fetch-header>
393
+ <wcs-fetch-header name="Accept" value="application/json"></wcs-fetch-header>
394
+ <wcs-fetch-body type="application/json">
395
+ {"name": "Tanaka", "email": "tanaka@example.com"}
396
+ </wcs-fetch-body>
397
+ </wcs-fetch>
398
+ ```
399
+
400
+ ## wc-bindable-protocol
401
+
402
+ Both `FetchCore` and `<wcs-fetch>` declare `wc-bindable-protocol` compliance, making them interoperable with any framework or component that supports the protocol.
403
+
404
+ ### Core (`FetchCore`)
405
+
406
+ `FetchCore` declares the bindable async state that any runtime can subscribe to:
407
+
408
+ ```typescript
409
+ static wcBindable = {
410
+ protocol: "wc-bindable",
411
+ version: 1,
412
+ properties: [
413
+ { name: "value", event: "wcs-fetch:response",
414
+ getter: (e) => e.detail.value },
415
+ { name: "loading", event: "wcs-fetch:loading-changed" },
416
+ { name: "error", event: "wcs-fetch:error" },
417
+ { name: "status", event: "wcs-fetch:response",
418
+ getter: (e) => e.detail.status },
419
+ ],
420
+ };
421
+ ```
422
+
423
+ Headless consumers call `core.fetch(url)` directly — no `trigger` needed.
424
+
425
+ ### Shell (`<wcs-fetch>`)
426
+
427
+ The Shell extends the Core declaration with `trigger` so binding systems can execute fetch declaratively:
428
+
429
+ ```typescript
430
+ static wcBindable = {
431
+ ...FetchCore.wcBindable,
432
+ properties: [
433
+ ...FetchCore.wcBindable.properties,
434
+ { name: "trigger", event: "wcs-fetch:trigger-changed" },
435
+ ],
436
+ };
437
+ ```
438
+
439
+ ## TypeScript Types
440
+
441
+ ```typescript
442
+ import type {
443
+ WcsFetchHttpError, WcsFetchCoreValues, WcsFetchValues
444
+ } from "@wcstack/fetch";
445
+ ```
446
+
447
+ ```typescript
448
+ // HTTP error (status >= 400)
449
+ interface WcsFetchHttpError {
450
+ status: number;
451
+ statusText: string;
452
+ body: string;
453
+ }
454
+
455
+ // Core (headless) — 4 async state properties
456
+ // T defaults to unknown; pass a type argument for typed `value`
457
+ interface WcsFetchCoreValues<T = unknown> {
458
+ value: T;
459
+ loading: boolean;
460
+ error: WcsFetchHttpError | Error | null;
461
+ status: number;
462
+ }
463
+
464
+ // Shell (<wcs-fetch>) — extends Core with trigger
465
+ interface WcsFetchValues<T = unknown> extends WcsFetchCoreValues<T> {
466
+ trigger: boolean;
467
+ }
468
+ ```
469
+
470
+ ## Why this works well with `@wcstack/state`
471
+
472
+ `@wcstack/state` uses path strings as the only contract between UI and state.
473
+ `<wcs-fetch>` fits this model naturally:
474
+
475
+ - state computes `url`
476
+ - `<wcs-fetch>` executes the request
477
+ - async results return as `value`, `loading`, `error`, `status`
478
+ - UI binds to those paths without writing fetch glue code
479
+
480
+ This makes async processing look like ordinary state updates.
481
+
482
+ ## Framework Integration
483
+
484
+ Since `<wcs-fetch>` is HAWC + `wc-bindable-protocol`, it works with any framework through thin adapters from `@wc-bindable/*`.
485
+
486
+ ### React
487
+
488
+ ```tsx
489
+ import { useWcBindable } from "@wc-bindable/react";
490
+ import type { WcsFetchValues } from "@wcstack/fetch";
491
+
492
+ interface User { id: number; name: string; }
493
+
494
+ function UserList() {
495
+ const [ref, { value: users, loading, error }] =
496
+ useWcBindable<HTMLElement, WcsFetchValues<User[]>>();
497
+
498
+ return (
499
+ <>
500
+ <wcs-fetch ref={ref} url="/api/users" />
501
+ {loading && <p>Loading...</p>}
502
+ {error && <p>Error</p>}
503
+ <ul>
504
+ {users?.map((user) => (
505
+ <li key={user.id}>{user.name}</li>
506
+ ))}
507
+ </ul>
508
+ </>
509
+ );
510
+ }
511
+ ```
512
+
513
+ ### Vue
514
+
515
+ ```vue
516
+ <script setup lang="ts">
517
+ import { useWcBindable } from "@wc-bindable/vue";
518
+ import type { WcsFetchValues } from "@wcstack/fetch";
519
+
520
+ interface User { id: number; name: string; }
521
+
522
+ const { ref, values } = useWcBindable<HTMLElement, WcsFetchValues<User[]>>();
523
+ </script>
524
+
525
+ <template>
526
+ <wcs-fetch :ref="ref" url="/api/users" />
527
+ <p v-if="values.loading">Loading...</p>
528
+ <p v-else-if="values.error">Error</p>
529
+ <ul v-else>
530
+ <li v-for="user in values.value" :key="user.id">{{ user.name }}</li>
531
+ </ul>
532
+ </template>
533
+ ```
534
+
535
+ ### Svelte
536
+
537
+ ```svelte
538
+ <script>
539
+ import { wcBindable } from "@wc-bindable/svelte";
540
+
541
+ let users = $state(null);
542
+ let loading = $state(false);
543
+ </script>
544
+
545
+ <wcs-fetch url="/api/users"
546
+ use:wcBindable={{ onUpdate: (name, v) => {
547
+ if (name === "value") users = v;
548
+ if (name === "loading") loading = v;
549
+ }}} />
550
+
551
+ {#if loading}
552
+ <p>Loading...</p>
553
+ {:else if users}
554
+ <ul>
555
+ {#each users as user (user.id)}
556
+ <li>{user.name}</li>
557
+ {/each}
558
+ </ul>
559
+ {/if}
560
+ ```
561
+
562
+ ### Solid
563
+
564
+ ```tsx
565
+ import { createWcBindable } from "@wc-bindable/solid";
566
+ import type { WcsFetchValues } from "@wcstack/fetch";
567
+
568
+ interface User { id: number; name: string; }
569
+
570
+ function UserList() {
571
+ const [values, directive] = createWcBindable<WcsFetchValues<User[]>>();
572
+
573
+ return (
574
+ <>
575
+ <wcs-fetch ref={directive} url="/api/users" />
576
+ <Show when={!values.loading} fallback={<p>Loading...</p>}>
577
+ <ul>
578
+ <For each={values.value}>{(user) => <li>{user.name}</li>}</For>
579
+ </ul>
580
+ </Show>
581
+ </>
582
+ );
583
+ }
584
+ ```
585
+
586
+ ### Vanilla — `bind()` directly
587
+
588
+ ```javascript
589
+ import { bind } from "@wc-bindable/core";
590
+
591
+ const fetchEl = document.querySelector("wcs-fetch");
592
+
593
+ bind(fetchEl, (name, value) => {
594
+ console.log(`${name} changed:`, value);
595
+ });
596
+ ```
597
+
598
+ ## Configuration
599
+
600
+ ```javascript
601
+ import { bootstrapFetch } from "@wcstack/fetch";
602
+
603
+ bootstrapFetch({
604
+ autoTrigger: true,
605
+ triggerAttribute: "data-fetchtarget",
606
+ tagNames: {
607
+ fetch: "wcs-fetch",
608
+ fetchHeader: "wcs-fetch-header",
609
+ fetchBody: "wcs-fetch-body",
610
+ },
611
+ });
612
+ ```
613
+
614
+ ## Design Notes
615
+
616
+ - `value`, `loading`, `error`, and `status` are **output state**
617
+ - `url`, `body`, and `trigger` are **input / command surface**
618
+ - `trigger` is intentionally one-way: writing `true` executes, reset emits completion
619
+ - `body` is reset to `null` after each `fetch()` call — set it again before each submission
620
+ - `manual` is useful when execution timing should be controlled explicitly
621
+ - HTML replace mode is optional; the primary wcstack pattern is state-driven binding
622
+
623
+ ## License
624
+
625
+ MIT
package/dist/auto.js ADDED
@@ -0,0 +1,3 @@
1
+ import { bootstrapFetch } from "./index.esm.js";
2
+
3
+ bootstrapFetch();
@@ -0,0 +1,3 @@
1
+ import { bootstrapFetch } from "./index.esm.min.js";
2
+
3
+ bootstrapFetch();