@xoong/data 0.0.42 → 0.0.44
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/package.json +2 -2
- package/src/view.js +47 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xoong/data",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"description": "xoong data layer: OpenAPI-driven client, auto cache, entity store, derived views, live updates. Depends only on @xoong/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@xoong/core": "0.0.
|
|
18
|
+
"@xoong/core": "0.0.44"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"src"
|
package/src/view.js
CHANGED
|
@@ -101,6 +101,7 @@ class XoView extends XoElement {
|
|
|
101
101
|
/** @type {Node[] | null} */ #tpl = null;
|
|
102
102
|
/** @type {(() => void)[]} */ #bound = [];
|
|
103
103
|
#params = signal(/** @type {Record<string, any>} */ ({}));
|
|
104
|
+
#warned = new Set();
|
|
104
105
|
#release = null;
|
|
105
106
|
/** Fetch the selected query again (xo-refresh). */ refresh = () => {};
|
|
106
107
|
/** @type {AbortController | null} */ #streamCtl = null;
|
|
@@ -254,6 +255,12 @@ class XoView extends XoElement {
|
|
|
254
255
|
q.value = null;
|
|
255
256
|
this.clearSelection(); // other rows are on screen now
|
|
256
257
|
if (!ref) return;
|
|
258
|
+
try { // a path parameter without a value: say so once (the symptom is a request to /users/{id} and an empty screen)
|
|
259
|
+
for (const m of client.op(ref.startsWith('/') ? `GET ${ref}` : ref).path.matchAll(/\{([^}]+)\}/g)) {
|
|
260
|
+
const v = params[m[1]];
|
|
261
|
+
if ((v == null || v === '') && !this.#warned.has(m[1])) { this.#warned.add(m[1]); console.warn(`xoong: <xo-view get="${ref}"> has no value for {${m[1]}}: add p-${m[1]}="..." (or p-${m[1]}=":${m[1]}" from a route)`); }
|
|
262
|
+
}
|
|
263
|
+
} catch { /* an unknown operation is reported by the query below */ }
|
|
257
264
|
try {
|
|
258
265
|
current = client.query(ref.startsWith('/') ? `GET ${ref}` : ref, params); // "/users" or an operationId
|
|
259
266
|
} catch (e) {
|
|
@@ -391,6 +398,16 @@ function bindNode(node, scope, ctx, disposers) {
|
|
|
391
398
|
el.removeAttribute(name);
|
|
392
399
|
continue;
|
|
393
400
|
}
|
|
401
|
+
if (BOOLEAN_ATTRS.has(name) && parts.length === 1 && typeof parts[0] !== 'string') { // hidden="{closed}": present when true, absent when false
|
|
402
|
+
const path = parts[0].path;
|
|
403
|
+
disposers.push(effect(() => {
|
|
404
|
+
const v = lookup(scope, path);
|
|
405
|
+
const on = Array.isArray(v) ? v.length > 0 : !!v && v !== 'false';
|
|
406
|
+
el.toggleAttribute(name, on);
|
|
407
|
+
if ((name === 'checked' || name === 'selected') && name in el) /** @type {any} */ (el)[name] = on; // the attribute only sets the default state
|
|
408
|
+
}));
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
394
411
|
disposers.push(effect(() => {
|
|
395
412
|
let v = evalParts(parts, scope, lookup);
|
|
396
413
|
if (URL_ATTRS.has(name) && !isSafeUrl(v)) v = 'about:blank';
|
|
@@ -398,9 +415,13 @@ function bindNode(node, scope, ctx, disposers) {
|
|
|
398
415
|
}));
|
|
399
416
|
}
|
|
400
417
|
}
|
|
418
|
+
if (el.localName === 'xo-view') return; // a nested view binds its own children against its own data when it starts; the outer pass must not touch them (its `p-*` attributes above are bound to the outer scope)
|
|
401
419
|
if (!translated) for (const child of [...el.childNodes]) bindNode(child, childScope, ctx, disposers); // a translated element's text is the message, not markup
|
|
402
420
|
}
|
|
403
421
|
|
|
422
|
+
/** Boolean attributes: `{flag}` as the whole value turns them on and off (an attribute with the value "false" would still count as set). */
|
|
423
|
+
const BOOLEAN_ATTRS = new Set(['hidden', 'disabled', 'checked', 'selected', 'readonly', 'required', 'open', 'multiple', 'inert', 'autofocus', 'controls', 'loop', 'muted', 'reversed']);
|
|
424
|
+
|
|
404
425
|
/** Attributes whose value is a URL: data must not smuggle in script schemes. */
|
|
405
426
|
const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction', 'poster', 'xlink:href', 'data', 'cite', 'srcset']);
|
|
406
427
|
const isScriptAttr = (name) => name.startsWith('on') || name === 'srcdoc';
|
|
@@ -445,24 +466,29 @@ function bindIf(el, scope, ctx, disposers) {
|
|
|
445
466
|
|
|
446
467
|
function bindEach(el, scope, ctx, disposers) {
|
|
447
468
|
const path = el.getAttribute('xo-each').trim();
|
|
469
|
+
// `<template xo-each="blocks">...</template>` repeats its children with no element of its own (a list of mixed blocks: h2, p, ul, figure)
|
|
470
|
+
const fragmentRows = el.localName === 'template';
|
|
448
471
|
const tpl = /** @type {Element} */ (el.cloneNode(true));
|
|
449
472
|
tpl.removeAttribute('xo-each');
|
|
450
473
|
const start = document.createComment('xo-each');
|
|
451
474
|
const end = document.createComment('/xo-each');
|
|
452
475
|
el.replaceWith(start, end);
|
|
453
|
-
/**
|
|
476
|
+
/** A row is the nodes from `first` to `last` (one element, or the children of a template between two comments). */
|
|
477
|
+
/** @typedef {{ first: Node, last: Node, stops: (() => void)[], index: ReturnType<typeof signal>, current: ReturnType<typeof signal>, frag?: DocumentFragment }} Row */
|
|
478
|
+
/** @type {Map<string, Row>} */
|
|
454
479
|
let rows = new Map();
|
|
480
|
+
const nodesOf = (/** @type {Row} */ row) => { const out = [row.first]; for (let n = row.first; n !== row.last && n.nextSibling; ) { n = /** @type {Node} */ (n.nextSibling); out.push(n); } return out; };
|
|
481
|
+
const dropRow = (/** @type {Row} */ row) => { row.stops.forEach((s) => s()); for (const n of nodesOf(row)) n.remove(); };
|
|
455
482
|
disposers.push(effect(() => {
|
|
456
483
|
const list = ctx.lookup(scope, path);
|
|
457
484
|
const items = (Array.isArray(list) ? list : []).filter((it) => !isRef(it) || ctx.client.store.has(it)); // drop deleted entities
|
|
458
485
|
// Do not subscribe this effect to row internals: bind rows untracked by their own effects.
|
|
459
486
|
const next = new Map();
|
|
460
|
-
let cursor = start;
|
|
487
|
+
/** @type {Node} */ let cursor = start;
|
|
461
488
|
items.forEach((item, i) => {
|
|
462
489
|
const key = isRef(item) ? item.$ref : `#${i}`;
|
|
463
490
|
let row = rows.get(key) ?? next.get(key);
|
|
464
491
|
if (!row) {
|
|
465
|
-
const node = /** @type {Element} */ (tpl.cloneNode(true));
|
|
466
492
|
const index = signal(i);
|
|
467
493
|
const current = signal(item);
|
|
468
494
|
// `$index` is the nearest row; `$i0`, `$i1` ... are the rows of the enclosing `xo-each`s, outermost first (names of nested array fields)
|
|
@@ -470,17 +496,29 @@ function bindEach(el, scope, ctx, disposers) {
|
|
|
470
496
|
for (let s = scope; s; s = s.parent) if (s.vars?.$index) depth++;
|
|
471
497
|
const itemScope = { value: () => current.value, parent: scope, vars: { $index: () => index.value, $n: () => index.value + 1, [`$i${depth}`]: () => index.value } };
|
|
472
498
|
const stops = [];
|
|
473
|
-
|
|
474
|
-
|
|
499
|
+
let first, last;
|
|
500
|
+
if (fragmentRows) {
|
|
501
|
+
const frag = document.createDocumentFragment();
|
|
502
|
+
first = document.createComment('xo-row');
|
|
503
|
+
last = document.createComment('/xo-row');
|
|
504
|
+
frag.append(first, .../** @type {HTMLTemplateElement} */ (tpl).content.cloneNode(true).childNodes, last);
|
|
505
|
+
batch(() => { for (const n of [...frag.childNodes]) if (n !== first && n !== last) bindNode(n, itemScope, ctx, stops); });
|
|
506
|
+
row = { first, last, stops, index, current, frag };
|
|
507
|
+
} else {
|
|
508
|
+
const node = /** @type {Element} */ (tpl.cloneNode(true));
|
|
509
|
+
batch(() => bindNode(node, itemScope, ctx, stops));
|
|
510
|
+
row = { first: node, last: node, stops, index, current };
|
|
511
|
+
}
|
|
475
512
|
} else batch(() => { row.index.value = i; row.current.value = item; }); // reused row follows its new item
|
|
476
513
|
next.set(key, row);
|
|
477
|
-
if (cursor.nextSibling !== row.
|
|
478
|
-
|
|
514
|
+
if (cursor.nextSibling !== row.first) cursor.after(...(row.frag ? [row.frag] : nodesOf(row)));
|
|
515
|
+
row.frag = undefined;
|
|
516
|
+
cursor = row.last;
|
|
479
517
|
});
|
|
480
|
-
for (const [key, row] of rows) if (!next.has(key))
|
|
518
|
+
for (const [key, row] of rows) if (!next.has(key)) dropRow(row);
|
|
481
519
|
rows = next;
|
|
482
520
|
}));
|
|
483
|
-
disposers.push(() => { for (const row of rows.values())
|
|
521
|
+
disposers.push(() => { for (const row of rows.values()) dropRow(row); rows.clear(); });
|
|
484
522
|
}
|
|
485
523
|
|
|
486
524
|
/** `@xoong/i18n` registers how `xo-t` elements are translated (the view supplies the scope for `{placeholders}`). */
|