@jarenjs/view 0.72.0 → 0.72.2

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.
@@ -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.0",
4
+ "version": "0.72.2",
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.0"
56
+ "@jarenjs/core": "^0.72.2"
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
- /** A controlled textarea's value is its text content in HTML markup. */
33
- const TEXTAREA_SKIP_PROPS = new Set([...SKIP_PROPS, 'value']);
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 ? TEXTAREA_SKIP_PROPS : SKIP_PROPS, policy, onUnsafe);
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
  }