@jarenjs/view 0.72.0 → 0.72.3
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/docs/VIEW-FORMAT.md +8 -0
- package/package.json +2 -2
- package/src/html.js +35 -6
package/docs/VIEW-FORMAT.md
CHANGED
|
@@ -228,6 +228,14 @@ pure: no state, no DOM, safe in any runtime.
|
|
|
228
228
|
In trusted mode, a `textarea` carrying a `value` prop serializes that
|
|
229
229
|
value as escaped text content, overriding its children; nullish values
|
|
230
230
|
produce empty text. A leading newline is preserved through HTML parsing.
|
|
231
|
+
For a `select` with a `value` prop, serialization places `selected` on
|
|
232
|
+
the matching `option`, overriding authored selections and omitting the
|
|
233
|
+
select's ineffective `value` attribute. A single select marks its first
|
|
234
|
+
match; a `multiple` select accepts an array. Matching uses string values,
|
|
235
|
+
including an option's normalized text when it has no value attribute,
|
|
236
|
+
and works through `optgroup`. HTML cannot encode a single select's
|
|
237
|
+
`selectedIndex = -1`: with no match, the browser may select its default
|
|
238
|
+
option until the DOM renderer reconciles the control.
|
|
231
239
|
Safe mode keeps its attribute-only policy (§8).
|
|
232
240
|
|
|
233
241
|
Hydration in 0.1 is a client-side first render into the same container
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/view",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.72.
|
|
4
|
+
"version": "0.72.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"ssr"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@jarenjs/core": "^0.72.
|
|
56
|
+
"@jarenjs/core": "^0.72.3"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "npm run build:types",
|
package/src/html.js
CHANGED
|
@@ -29,12 +29,20 @@ const VOID_ELEMENTS = new Set([
|
|
|
29
29
|
/** Props that never serialize to markup. */
|
|
30
30
|
const SKIP_PROPS = new Set(['key', 'on', 'memo']);
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
const
|
|
32
|
+
/** Controlled textareas/selects express their value through their children. */
|
|
33
|
+
const VALUE_SKIP_PROPS = new Set([...SKIP_PROPS, 'value']);
|
|
34
|
+
const OPTION_SKIP_PROPS = new Set([...SKIP_PROPS, 'selected']);
|
|
34
35
|
|
|
35
36
|
/** Widget-vnode props that configure the widget, not the host element. */
|
|
36
37
|
const WIDGET_SKIP_PROPS = new Set(['key', 'on', 'memo', 'name', 'props', 'tag']);
|
|
37
38
|
|
|
39
|
+
/** The text of an option whose value attribute is absent. @param {any} vnode */
|
|
40
|
+
function optionText(vnode) {
|
|
41
|
+
if (isTextNode(vnode)) return String(vnode);
|
|
42
|
+
if (!isElementNode(vnode) || vnode[0] === 'script') return '';
|
|
43
|
+
return childrenOf(vnode).map(optionText).join('');
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
/**
|
|
39
47
|
* Escape text content: `&`, `<`, `>`.
|
|
40
48
|
* @param {string} text
|
|
@@ -142,9 +150,10 @@ export function renderToString(vnode, options = {}) {
|
|
|
142
150
|
* @param {Record<string, { ssr?: (props: any) => any }> | undefined} widgets
|
|
143
151
|
* @param {import('./safe.js').SafePolicy | null} policy
|
|
144
152
|
* @param {((info: { kind: string, name: string }) => void) | null} onUnsafe
|
|
153
|
+
* @param {{values: Set<string>, multiple: boolean, matched: boolean} | null} [selection]
|
|
145
154
|
* @returns {string}
|
|
146
155
|
*/
|
|
147
|
-
function renderNode(vnode, widgets, policy, onUnsafe) {
|
|
156
|
+
function renderNode(vnode, widgets, policy, onUnsafe, selection = null) {
|
|
148
157
|
if (isTextNode(vnode)) {
|
|
149
158
|
return escapeText(String(vnode));
|
|
150
159
|
}
|
|
@@ -170,7 +179,7 @@ function renderNode(vnode, widgets, policy, onUnsafe) {
|
|
|
170
179
|
// `ssr()` (the documented behavior for both)
|
|
171
180
|
const ssr = def !== undefined ? def.ssr : undefined;
|
|
172
181
|
const inner = ssr !== undefined
|
|
173
|
-
? renderNode(ssr.call(def, props.props ?? null), widgets, policy, onUnsafe)
|
|
182
|
+
? renderNode(ssr.call(def, props.props ?? null), widgets, policy, onUnsafe, selection)
|
|
174
183
|
: '';
|
|
175
184
|
return '<' + hostTag + serializeProps(props, WIDGET_SKIP_PROPS, policy, onUnsafe) + '>'
|
|
176
185
|
+ inner + '</' + hostTag + '>';
|
|
@@ -188,8 +197,28 @@ function renderNode(vnode, widgets, policy, onUnsafe) {
|
|
|
188
197
|
onUnsafe({ kind: 'event', name: 'on' });
|
|
189
198
|
}
|
|
190
199
|
const controlledTextarea = policy === null && tag === 'textarea' && 'value' in props;
|
|
200
|
+
const controlledSelect = policy === null && tag === 'select' && 'value' in props;
|
|
201
|
+
if (tag === 'select') {
|
|
202
|
+
const multiple = props.multiple != null && props.multiple !== false;
|
|
203
|
+
selection = controlledSelect ? {
|
|
204
|
+
values: new Set(multiple && Array.isArray(props.value)
|
|
205
|
+
? props.value.map(String) : [props.value == null ? '' : String(props.value)]),
|
|
206
|
+
multiple: multiple && Array.isArray(props.value), matched: false,
|
|
207
|
+
} : null;
|
|
208
|
+
}
|
|
209
|
+
const controlledOption = tag === 'option' && selection !== null;
|
|
191
210
|
let out = '<' + tag + serializeProps(props,
|
|
192
|
-
controlledTextarea
|
|
211
|
+
controlledTextarea || controlledSelect ? VALUE_SKIP_PROPS
|
|
212
|
+
: controlledOption ? OPTION_SKIP_PROPS : SKIP_PROPS, policy, onUnsafe);
|
|
213
|
+
if (controlledOption) {
|
|
214
|
+
const value = props.value == null || props.value === false
|
|
215
|
+
? optionText(vnode).replace(/[\t\n\f\r ]+/g, ' ').replace(/^ | $/g, '')
|
|
216
|
+
: props.value === true ? '' : String(props.value);
|
|
217
|
+
if (selection.values.has(value) && (selection.multiple || !selection.matched)) {
|
|
218
|
+
out += ' selected';
|
|
219
|
+
selection.matched = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
193
222
|
if (VOID_ELEMENTS.has(tag)) {
|
|
194
223
|
return out + '>';
|
|
195
224
|
}
|
|
@@ -202,7 +231,7 @@ function renderNode(vnode, widgets, policy, onUnsafe) {
|
|
|
202
231
|
}
|
|
203
232
|
const children = childrenOf(vnode);
|
|
204
233
|
for (let i = 0; i < children.length; i++) {
|
|
205
|
-
out += renderNode(children[i], widgets, policy, onUnsafe);
|
|
234
|
+
out += renderNode(children[i], widgets, policy, onUnsafe, selection);
|
|
206
235
|
}
|
|
207
236
|
return out + '</' + tag + '>';
|
|
208
237
|
}
|