@webjsdev/core 0.7.62 → 0.7.63
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/webjs-core-browser.js +4 -4
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core-testing.js +17 -17
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +7 -7
- package/dist/webjs-core.js.map +2 -2
- package/package.json +1 -1
- package/src/binding-prefixes.js +28 -0
- package/src/render-client/parts.js +6 -2
- package/src/render-client/reconciler.js +14 -7
- package/src/render-client/template-compiler.js +3 -3
- package/src/render-server/template-renderer.js +11 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.63",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The runtime for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Ships the html and css template tags, the WebComponent base class, signals, directives, and the isomorphic renderers.",
|
|
6
6
|
"types": "./index.d.ts",
|
package/src/binding-prefixes.js
CHANGED
|
@@ -40,3 +40,31 @@ export const BINDING_PREFIXES = Object.freeze({
|
|
|
40
40
|
export function isBindingPrefix(ch) {
|
|
41
41
|
return Object.prototype.hasOwnProperty.call(BINDING_PREFIXES, ch);
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What a PLAIN attribute hole (`name=${value}`, unquoted, the whole value)
|
|
46
|
+
* writes, or `null` when the attribute is omitted. The one rule both renderers
|
|
47
|
+
* follow, so the server never serves an attribute the client then removes
|
|
48
|
+
* (#1573):
|
|
49
|
+
*
|
|
50
|
+
* - `null` / `undefined` omit the attribute. This is the "omit" value, so a
|
|
51
|
+
* conditional attribute is `aria-current=${active ? 'page' : null}`.
|
|
52
|
+
* - `false` omits it too, EXCEPT on an `aria-*` attribute, where it writes
|
|
53
|
+
* `"false"`. For a tri-state ARIA attribute (`aria-expanded`,
|
|
54
|
+
* `aria-pressed`, `aria-checked`) `"false"` and absent mean different
|
|
55
|
+
* things, so the value an author passes is the value the user agent gets.
|
|
56
|
+
* - anything else is `String(value)`.
|
|
57
|
+
*
|
|
58
|
+
* A quoted or mixed value (`title="${x}"`, `class="a ${x}"`) is not a plain
|
|
59
|
+
* hole: the author wrote the attribute statically, so it stays and a nullish
|
|
60
|
+
* piece reads as empty text.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} name the attribute name as written
|
|
63
|
+
* @param {unknown} value the hole's value (`live()` already unwrapped)
|
|
64
|
+
* @returns {string | null}
|
|
65
|
+
*/
|
|
66
|
+
export function attrHoleValue(name, value) {
|
|
67
|
+
if (value == null) return null;
|
|
68
|
+
if (value === false) return /^aria-/i.test(name) ? 'false' : null;
|
|
69
|
+
return String(value);
|
|
70
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
SLOT_FALLBACK_FRAG, SLOT_STATE, SLOT_OWNER, applySlotAssignments, withRendererWrites,
|
|
12
12
|
} from '../slot.js';
|
|
13
13
|
import { compile, templateCache } from './template-compiler.js';
|
|
14
|
+
import { attrHoleValue } from '../binding-prefixes.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The container the in-progress `render()` is committing into, or null
|
|
@@ -203,7 +204,10 @@ export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb)
|
|
|
203
204
|
applyChild(part, value, reconcileFormActionsCb);
|
|
204
205
|
break;
|
|
205
206
|
case 'attr': {
|
|
206
|
-
|
|
207
|
+
// #1573: the shared rule, so this agrees with SSR. `null` / `undefined`
|
|
208
|
+
// remove, `false` removes except on aria-*, where it writes "false".
|
|
209
|
+
const written = attrHoleValue(part.name, value);
|
|
210
|
+
if (written === null) part.el.removeAttribute(part.name);
|
|
207
211
|
else if (isBoundFormAction(value, part.name, part.el.localName)) {
|
|
208
212
|
// #1155: the ONE supported form-action binding, applied to the live
|
|
209
213
|
// form exactly as SSR wrote it. A component that ships re-renders its
|
|
@@ -220,7 +224,7 @@ export function applyPart(part, value, _prev, allValues, reconcileFormActionsCb)
|
|
|
220
224
|
// (mirrors the SSR guard, so a client re-render cannot write a
|
|
221
225
|
// server action's source into the live DOM).
|
|
222
226
|
assertNotFunctionActionAttr(value, part.name, part.el.localName);
|
|
223
|
-
part.el.setAttribute(part.name,
|
|
227
|
+
part.el.setAttribute(part.name, written);
|
|
224
228
|
}
|
|
225
229
|
break;
|
|
226
230
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
assertIdentifiableAction, FORM_ACTION_FIELD,
|
|
8
8
|
} from '../form-action.js';
|
|
9
9
|
import { isLive } from '../directives.js';
|
|
10
|
+
import { attrHoleValue } from '../binding-prefixes.js';
|
|
10
11
|
import { RENDERING, SLOT_OWNER, SLOT_STATE, drainRendererBackstop, rescueAssignedNodes } from '../slot.js';
|
|
11
12
|
import { compile, submitterActionBindings, INSTANCE } from './template-compiler.js';
|
|
12
13
|
import {
|
|
@@ -132,13 +133,17 @@ function reconcileFormActions(formActions, bound, values) {
|
|
|
132
133
|
rec,
|
|
133
134
|
);
|
|
134
135
|
} else {
|
|
135
|
-
// What SSR would have emitted for this pass.
|
|
136
|
-
// emits
|
|
137
|
-
//
|
|
138
|
-
// one
|
|
136
|
+
// What SSR would have emitted for this pass. A plain attribute hole
|
|
137
|
+
// emits unless `attrHoleValue` omits it (`null` / `undefined` /
|
|
138
|
+
// `false`, since `name` and `value` are not aria-*, #1573), a mixed
|
|
139
|
+
// one always emits, and a boolean hole emits only when truthy. Both
|
|
140
|
+
// identity channels ask the same question, through one predicate, so
|
|
141
|
+
// they cannot drift apart again.
|
|
139
142
|
const emits = (parts) => parts.some((np) => (np.kind === 'bool'
|
|
140
143
|
? !!resolveHoleValue(values[np.i])
|
|
141
|
-
:
|
|
144
|
+
: np.kind === 'attr'
|
|
145
|
+
? attrHoleValue('name', resolveHoleValue(values[np.i])) !== null
|
|
146
|
+
: true));
|
|
142
147
|
reconcileSubmitterAction(
|
|
143
148
|
/** @type any */ (part.el), val, rec,
|
|
144
149
|
emits(rec.nameParts), emits(rec.valueParts),
|
|
@@ -267,8 +272,10 @@ function effectiveFormAttr(attrParts, staticValue, values) {
|
|
|
267
272
|
for (const p of attrParts) {
|
|
268
273
|
if (p.kind === 'bool') return resolveHoleValue(values[p.i]) ? '' : ABSENT;
|
|
269
274
|
if (p.kind === 'attr') {
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
// #1573: an omitted plain hole is ABSENT, exactly as SSR emits it, so
|
|
276
|
+
// `method=${null}` leaves the framework to supply `method="post"`.
|
|
277
|
+
const w = attrHoleValue('method', resolveHoleValue(values[p.i]));
|
|
278
|
+
return w === null ? ABSENT : w;
|
|
272
279
|
}
|
|
273
280
|
// A mixed attribute is the concatenation of its static pieces and EVERY one
|
|
274
281
|
// of its holes, so the anchor's own value is only part of the answer.
|
|
@@ -440,9 +440,9 @@ function buildFormActionRecord(el, onEl, parts) {
|
|
|
440
440
|
/** Prop bindings that cannot converge with SSR; refused when actually bound. */
|
|
441
441
|
const propAttrs = [];
|
|
442
442
|
// `name` holes on a submitter, recorded WITH their kind. Whether one occupies
|
|
443
|
-
// the identity's channel is not a property of the hole alone: SSR
|
|
444
|
-
//
|
|
445
|
-
//
|
|
443
|
+
// the identity's channel is not a property of the hole alone: SSR omits an
|
|
444
|
+
// attribute hole that resolved to `null` / `undefined` / `false` (#1573),
|
|
445
|
+
// emits nothing for a FALSY boolean hole, and nothing for an `@name` listener. Asking
|
|
446
446
|
// only "is there a part called name" therefore refused templates SSR renders
|
|
447
447
|
// happily, which is the render-on-the-server-throw-on-hydration direction.
|
|
448
448
|
// The kinds travel with the record and `reconcileFormActions` resolves them.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { html, isTemplate } from '../html.js';
|
|
2
|
-
import { BINDING_PREFIXES } from '../binding-prefixes.js';
|
|
2
|
+
import { BINDING_PREFIXES, attrHoleValue } from '../binding-prefixes.js';
|
|
3
3
|
import { escapeText, escapeAttr } from '../escape.js';
|
|
4
4
|
import {
|
|
5
5
|
assertNotFunctionActionAttr, assertNotFunctionReflectedActionProp,
|
|
@@ -542,7 +542,12 @@ export async function renderTemplate(tr, ctx) {
|
|
|
542
542
|
// #1154: never stringify a function into action=/formaction= (it
|
|
543
543
|
// would serialize a server action's source into the served HTML).
|
|
544
544
|
assertNotFunctionActionAttr(val, attrName, currentTag);
|
|
545
|
-
|
|
545
|
+
// #1573: the client's rule, not a stringify. `null` / `undefined`
|
|
546
|
+
// (and `false` off an aria-* name) omit the attribute, which is
|
|
547
|
+
// already in `out` from `attrStart`, so slice it back off.
|
|
548
|
+
const written = attrHoleValue(attrName, val);
|
|
549
|
+
if (written === null) out = out.slice(0, attrStart);
|
|
550
|
+
else out += `"${escapeAttr(written)}"`;
|
|
546
551
|
state = 'in-tag';
|
|
547
552
|
attrName = '';
|
|
548
553
|
}
|
|
@@ -922,7 +927,10 @@ export async function streamTemplate(tr, ctx, controller) {
|
|
|
922
927
|
// `renderToStream(v, { ssr: false })`, which no page render uses, so
|
|
923
928
|
// this covers the public API surface rather than a page leak.
|
|
924
929
|
assertNotFunctionActionAttr(val, attrName, currentTag);
|
|
925
|
-
|
|
930
|
+
// #1573: the same omission rule as the buffered renderer above.
|
|
931
|
+
const written = attrHoleValue(attrName, val);
|
|
932
|
+
if (written === null) buf = buf.slice(0, attrStart);
|
|
933
|
+
else buf += `"${escapeAttr(written)}"`;
|
|
926
934
|
state = 'in-tag';
|
|
927
935
|
attrName = '';
|
|
928
936
|
}
|