@webjsdev/core 0.7.1

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/src/repeat.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `repeat()`: keyed list directive.
3
+ *
4
+ * Usage in a template:
5
+ * ```js
6
+ * import { html, repeat } from '@webjsdev/core';
7
+ * html`<ul>${repeat(items, (it) => it.id, (it) => html`<li>${it.title}</li>`)}</ul>`;
8
+ * ```
9
+ *
10
+ * Without `repeat()`, an array re-render rebuilds every child (losing focus,
11
+ * selection, and scroll). With `repeat()`, the client renderer maintains a
12
+ * `Map<key, TemplateInstance>`: matched items update in place, new items
13
+ * mount fresh, removed items unmount, and the surviving DOM nodes are moved
14
+ * (not recreated) when the order changes.
15
+ *
16
+ * Keys must be stable and unique within the call. Bad keys (array indices)
17
+ * defeat the purpose; prefer a stable id from the data.
18
+ *
19
+ * On the server this marker is just iterated: order matters, keys don't.
20
+ */
21
+
22
+ const REPEAT = Symbol.for('webjs.repeat');
23
+
24
+ /**
25
+ * @template T
26
+ * @param {Iterable<T>} items
27
+ * @param {(item: T, i: number) => string | number} keyFn
28
+ * @param {(item: T, i: number) => unknown} templateFn
29
+ */
30
+ export function repeat(items, keyFn, templateFn) {
31
+ return { [REPEAT]: true, items: [...items], keyFn, templateFn };
32
+ }
33
+
34
+ /** @param {unknown} x */
35
+ export function isRepeat(x) {
36
+ return !!x && typeof x === 'object' && /** @type any */ (x)[REPEAT] === true;
37
+ }
@@ -0,0 +1,70 @@
1
+ import { stringify as wjStringify, parse as wjParse } from './serialize.js';
2
+
3
+ const RPC_CONTENT_TYPE = 'application/vnd.webjs+json';
4
+
5
+ /**
6
+ * `richFetch(url, init?)`: drop-in `fetch` for calling your own API routes
7
+ * when you want rich types to round-trip (`Date`, `Map`, `Set`, `BigInt`,
8
+ * `TypedArray`, `Blob`, `File`, `FormData`, cycles).
9
+ *
10
+ * - Adds `Accept: application/vnd.webjs+json` to the request so a
11
+ * server-side `json(data)` helper encodes with the webjs serializer.
12
+ * - If the response content-type matches, decodes with the webjs
13
+ * serializer and returns the rich value.
14
+ * - Otherwise decodes with plain `.json()` (so any route you haven't
15
+ * opted in on still works).
16
+ * - Request bodies: if `init.body` is a plain object (not a string,
17
+ * FormData/Blob/ArrayBuffer/ReadableStream/URLSearchParams), it is
18
+ * encoded with the webjs serializer and the request content-type is
19
+ * set to our vendor type.
20
+ *
21
+ * ```js
22
+ * import { richFetch } from '@webjsdev/core';
23
+ * const posts = await richFetch('/api/posts'); // Post[]; createdAt is Date
24
+ * await richFetch('/api/posts', { // rich body
25
+ * method: 'POST',
26
+ * body: { publishAt: new Date(2026, 0, 1) },
27
+ * });
28
+ * ```
29
+ *
30
+ * @template T
31
+ * @param {string | URL} url
32
+ * @param {RequestInit & { body?: any }} [init]
33
+ * @returns {Promise<T>}
34
+ */
35
+ export async function richFetch(url, init = {}) {
36
+ const headers = new Headers(init.headers || {});
37
+ if (!headers.has('accept')) headers.set('accept', RPC_CONTENT_TYPE);
38
+
39
+ let body = init.body;
40
+ if (
41
+ body != null &&
42
+ typeof body === 'object' &&
43
+ !(body instanceof FormData) &&
44
+ !(body instanceof Blob) &&
45
+ !(body instanceof ArrayBuffer) &&
46
+ !(body instanceof URLSearchParams) &&
47
+ !(typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) &&
48
+ !ArrayBuffer.isView(body)
49
+ ) {
50
+ body = await wjStringify(body);
51
+ if (!headers.has('content-type')) headers.set('content-type', RPC_CONTENT_TYPE);
52
+ }
53
+
54
+ const res = await fetch(url, { ...init, headers, body });
55
+ const ct = res.headers.get('content-type') || '';
56
+ const text = await res.text();
57
+ const parsed = ct.includes(RPC_CONTENT_TYPE)
58
+ ? (text ? wjParse(text) : null)
59
+ : ct.includes('application/json')
60
+ ? (text ? JSON.parse(text) : null)
61
+ : text;
62
+ if (!res.ok) {
63
+ const msg = (parsed && parsed.error) || `richFetch ${url} -> ${res.status}`;
64
+ const err = /** @type any */ (new Error(msg));
65
+ err.status = res.status;
66
+ err.body = parsed;
67
+ throw err;
68
+ }
69
+ return parsed;
70
+ }