@barefootjs/client 0.1.3 → 0.3.0

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.
@@ -314,6 +314,156 @@ var BF_KEY = "data-key";
314
314
  var BF_ASYNC = "bf-async";
315
315
  var BF_ASYNC_RESOLVE = "bf-async-resolve";
316
316
  var BF_PARENT_SCOPE_PLACEHOLDER = "__BF_PARENT_SCOPE__";
317
+ // ../shared/src/dom-prop.ts
318
+ var BOOLEAN_ATTRS = new Set([
319
+ "checked",
320
+ "disabled",
321
+ "readonly",
322
+ "selected",
323
+ "required",
324
+ "hidden",
325
+ "autofocus",
326
+ "autoplay",
327
+ "controls",
328
+ "loop",
329
+ "muted",
330
+ "open",
331
+ "multiple",
332
+ "novalidate",
333
+ "formnovalidate"
334
+ ]);
335
+ var SVG_CAMEL_TO_KEBAB = {
336
+ strokeWidth: "stroke-width",
337
+ strokeLinecap: "stroke-linecap",
338
+ strokeLinejoin: "stroke-linejoin",
339
+ strokeDasharray: "stroke-dasharray",
340
+ strokeDashoffset: "stroke-dashoffset",
341
+ strokeMiterlimit: "stroke-miterlimit",
342
+ strokeOpacity: "stroke-opacity",
343
+ fillOpacity: "fill-opacity",
344
+ fillRule: "fill-rule",
345
+ stopColor: "stop-color",
346
+ stopOpacity: "stop-opacity",
347
+ textAnchor: "text-anchor",
348
+ dominantBaseline: "dominant-baseline",
349
+ alignmentBaseline: "alignment-baseline",
350
+ fontFamily: "font-family",
351
+ fontSize: "font-size",
352
+ fontWeight: "font-weight",
353
+ fontStyle: "font-style",
354
+ letterSpacing: "letter-spacing",
355
+ wordSpacing: "word-spacing",
356
+ pointerEvents: "pointer-events",
357
+ vectorEffect: "vector-effect",
358
+ colorInterpolation: "color-interpolation",
359
+ clipPath: "clip-path",
360
+ clipRule: "clip-rule",
361
+ markerStart: "marker-start",
362
+ markerMid: "marker-mid",
363
+ markerEnd: "marker-end"
364
+ };
365
+ var SVG_XML_CAMEL_ATTRS = new Set([
366
+ "allowReorder",
367
+ "attributeName",
368
+ "attributeType",
369
+ "autoReverse",
370
+ "baseFrequency",
371
+ "baseProfile",
372
+ "calcMode",
373
+ "clipPathUnits",
374
+ "contentScriptType",
375
+ "contentStyleType",
376
+ "diffuseConstant",
377
+ "edgeMode",
378
+ "externalResourcesRequired",
379
+ "filterRes",
380
+ "filterUnits",
381
+ "glyphRef",
382
+ "gradientTransform",
383
+ "gradientUnits",
384
+ "kernelMatrix",
385
+ "kernelUnitLength",
386
+ "keyPoints",
387
+ "keySplines",
388
+ "keyTimes",
389
+ "lengthAdjust",
390
+ "limitingConeAngle",
391
+ "markerHeight",
392
+ "markerUnits",
393
+ "markerWidth",
394
+ "maskContentUnits",
395
+ "maskUnits",
396
+ "numOctaves",
397
+ "pathLength",
398
+ "patternContentUnits",
399
+ "patternTransform",
400
+ "patternUnits",
401
+ "pointsAtX",
402
+ "pointsAtY",
403
+ "pointsAtZ",
404
+ "preserveAlpha",
405
+ "preserveAspectRatio",
406
+ "primitiveUnits",
407
+ "refX",
408
+ "refY",
409
+ "repeatCount",
410
+ "repeatDur",
411
+ "requiredExtensions",
412
+ "requiredFeatures",
413
+ "specularConstant",
414
+ "specularExponent",
415
+ "spreadMethod",
416
+ "startOffset",
417
+ "stdDeviation",
418
+ "stitchTiles",
419
+ "surfaceScale",
420
+ "systemLanguage",
421
+ "tableValues",
422
+ "targetX",
423
+ "targetY",
424
+ "textLength",
425
+ "viewBox",
426
+ "viewTarget",
427
+ "xChannelSelector",
428
+ "yChannelSelector",
429
+ "zoomAndPan"
430
+ ]);
431
+ function isEventProp(key) {
432
+ return key.length > 2 && key[0] === "o" && key[1] === "n" && key[2] >= "A" && key[2] <= "Z";
433
+ }
434
+ function classifyDOMProp(key) {
435
+ if (key === "children")
436
+ return { kind: "skip", attrName: key };
437
+ if (key === "ref")
438
+ return { kind: "ref", attrName: key };
439
+ if (isEventProp(key))
440
+ return { kind: "event", attrName: key };
441
+ const attrName = toHTMLAttrNameRuntime(key);
442
+ if (attrName === "style")
443
+ return { kind: "style", attrName };
444
+ if (attrName === "value")
445
+ return { kind: "property", attrName };
446
+ if (attrName === "checked")
447
+ return { kind: "property", attrName };
448
+ if (BOOLEAN_ATTRS.has(attrName.toLowerCase()))
449
+ return { kind: "boolean", attrName };
450
+ return { kind: "attr", attrName };
451
+ }
452
+ function toHTMLAttrNameRuntime(key) {
453
+ if (key === "className")
454
+ return "class";
455
+ if (key === "htmlFor")
456
+ return "for";
457
+ const svgKebab = SVG_CAMEL_TO_KEBAB[key];
458
+ if (svgKebab !== undefined)
459
+ return svgKebab;
460
+ if (SVG_XML_CAMEL_ATTRS.has(key))
461
+ return key;
462
+ if (key.startsWith("data") || key.startsWith("aria")) {
463
+ return key.replace(/([A-Z])/g, "-$1").toLowerCase();
464
+ }
465
+ return key;
466
+ }
317
467
  // src/context.ts
318
468
  function createContext(defaultValue) {
319
469
  return {
@@ -957,6 +1107,8 @@ function hydrateCommentScope(comment) {
957
1107
  const proxyEl = nextElementSibling2(comment) ?? comment.parentElement;
958
1108
  if (!proxyEl)
959
1109
  return;
1110
+ if (hydratedScopes.has(proxyEl))
1111
+ return;
960
1112
  commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId });
961
1113
  const parsed = parseProps(propsJson || null, `comment scope ${scopeId}`);
962
1114
  const props = parsed[name] ?? {};
@@ -1010,9 +1162,14 @@ function createComponent(nameOrDef, props, key, slot) {
1010
1162
  }
1011
1163
  return result;
1012
1164
  });
1165
+ const def = getRegisteredDef(name);
1166
+ const isCommentWrapper = def?.comment === true;
1167
+ const scopeId = isCommentWrapper ? null : `${name}_${generateId()}`;
1013
1168
  const prevParentScopeId = _parentScopeId;
1014
1169
  if (slot?.parent) {
1015
1170
  _parentScopeId = slot.parent;
1171
+ } else if (scopeId) {
1172
+ _parentScopeId = scopeId;
1016
1173
  }
1017
1174
  let html;
1018
1175
  try {
@@ -1025,10 +1182,8 @@ function createComponent(nameOrDef, props, key, slot) {
1025
1182
  console.warn(`[BarefootJS] Template returned empty HTML for component: ${name}`);
1026
1183
  return createPlaceholder(name, key);
1027
1184
  }
1028
- const def = getRegisteredDef(name);
1029
- const isCommentWrapper = def?.comment === true;
1030
- if (!isCommentWrapper) {
1031
- element.setAttribute(BF_SCOPE, `${name}_${generateId()}`);
1185
+ if (scopeId) {
1186
+ element.setAttribute(BF_SCOPE, scopeId);
1032
1187
  }
1033
1188
  if (slot) {
1034
1189
  if (slot.parent)
@@ -1672,6 +1827,17 @@ function mapArray(accessor, container, getKey, renderItem, markerId) {
1672
1827
  scopes.set(key, scope);
1673
1828
  insertScope(scope, container, anchor);
1674
1829
  }
1830
+ for (let i = items.length;i < existingRanges.length; i++) {
1831
+ const range = existingRanges[i];
1832
+ if (range.startMarker?.parentNode)
1833
+ range.startMarker.remove();
1834
+ if (range.primaryEl.parentNode)
1835
+ range.primaryEl.remove();
1836
+ for (const ex of range.extras) {
1837
+ if (ex.parentNode)
1838
+ ex.remove();
1839
+ }
1840
+ }
1675
1841
  return;
1676
1842
  }
1677
1843
  }
@@ -1766,13 +1932,6 @@ function styleToCss(value) {
1766
1932
  }
1767
1933
 
1768
1934
  // src/runtime/apply-rest-attrs.ts
1769
- function toAttrName(key) {
1770
- if (key === "className")
1771
- return "class";
1772
- if (key === "htmlFor")
1773
- return "for";
1774
- return key.replace(/([A-Z])/g, "-$1").toLowerCase();
1775
- }
1776
1935
  var jsxToDomEventMap = { doubleclick: "dblclick" };
1777
1936
  function toEventName(jsxPropName) {
1778
1937
  const raw = (jsxPropName[2].toLowerCase() + jsxPropName.slice(3)).toLowerCase();
@@ -1780,127 +1939,61 @@ function toEventName(jsxPropName) {
1780
1939
  }
1781
1940
  function applyRestAttrs(el, source, excludeKeys) {
1782
1941
  const exclude = new Set(excludeKeys);
1942
+ const classified = [];
1783
1943
  for (const key of Object.keys(source)) {
1784
1944
  if (exclude.has(key))
1785
1945
  continue;
1786
- if (key === "ref") {
1946
+ classified.push({ key, c: classifyDOMProp(key) });
1947
+ }
1948
+ for (const { key, c } of classified) {
1949
+ if (c.kind === "ref") {
1787
1950
  const ref = source[key];
1788
1951
  if (typeof ref === "function")
1789
1952
  ref(el);
1790
- continue;
1791
- }
1792
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase()) {
1953
+ } else if (c.kind === "event") {
1793
1954
  const handler = source[key];
1794
1955
  if (typeof handler === "function") {
1795
1956
  el.addEventListener(toEventName(key), handler);
1796
1957
  }
1797
1958
  }
1798
1959
  }
1960
+ const attrEntries = classified.filter(({ c }) => c.kind !== "ref" && c.kind !== "event" && c.kind !== "skip");
1799
1961
  createEffect(() => {
1800
- for (const key of Object.keys(source)) {
1801
- if (exclude.has(key))
1802
- continue;
1803
- if (key === "ref")
1804
- continue;
1805
- if (key === "children")
1806
- continue;
1807
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
1808
- continue;
1962
+ for (const { key, c } of attrEntries) {
1809
1963
  const value = source[key];
1810
- const attr = toAttrName(key);
1811
1964
  if (value != null && value !== false) {
1812
- if (attr === "value" && "value" in el) {
1965
+ if (c.kind === "property" && c.attrName === "value" && "value" in el) {
1813
1966
  const strVal = String(value);
1814
1967
  if (el.value !== strVal)
1815
1968
  el.value = strVal;
1816
- } else if (attr === "checked" && "checked" in el) {
1969
+ } else if (c.kind === "property" && c.attrName === "checked" && "checked" in el) {
1817
1970
  el.checked = !!value;
1818
- } else if (attr === "style") {
1971
+ } else if (c.kind === "boolean") {
1972
+ el.setAttribute(c.attrName, "");
1973
+ } else if (c.kind === "style") {
1819
1974
  const css = styleToCss(value);
1820
1975
  if (css == null)
1821
1976
  el.removeAttribute("style");
1822
1977
  else
1823
1978
  el.setAttribute("style", css);
1824
1979
  } else {
1825
- el.setAttribute(attr, String(value));
1980
+ el.setAttribute(c.attrName, String(value));
1826
1981
  }
1827
1982
  } else {
1828
- if (attr === "checked" && "checked" in el) {
1983
+ if (c.kind === "property" && c.attrName === "value" && "value" in el) {
1984
+ el.value = "";
1985
+ } else if (c.kind === "property" && c.attrName === "checked" && "checked" in el) {
1829
1986
  el.checked = false;
1987
+ } else if (c.kind === "boolean") {
1988
+ el.removeAttribute(c.attrName);
1830
1989
  } else {
1831
- el.removeAttribute(attr);
1990
+ el.removeAttribute(c.attrName);
1832
1991
  }
1833
1992
  }
1834
1993
  }
1835
1994
  });
1836
1995
  }
1837
1996
  // src/runtime/spread-attrs.ts
1838
- var SVG_CAMEL_CASE_ATTRS = new Set([
1839
- "allowReorder",
1840
- "attributeName",
1841
- "attributeType",
1842
- "autoReverse",
1843
- "baseFrequency",
1844
- "baseProfile",
1845
- "calcMode",
1846
- "clipPathUnits",
1847
- "contentScriptType",
1848
- "contentStyleType",
1849
- "diffuseConstant",
1850
- "edgeMode",
1851
- "externalResourcesRequired",
1852
- "filterRes",
1853
- "filterUnits",
1854
- "glyphRef",
1855
- "gradientTransform",
1856
- "gradientUnits",
1857
- "kernelMatrix",
1858
- "kernelUnitLength",
1859
- "keyPoints",
1860
- "keySplines",
1861
- "keyTimes",
1862
- "lengthAdjust",
1863
- "limitingConeAngle",
1864
- "markerHeight",
1865
- "markerUnits",
1866
- "markerWidth",
1867
- "maskContentUnits",
1868
- "maskUnits",
1869
- "numOctaves",
1870
- "pathLength",
1871
- "patternContentUnits",
1872
- "patternTransform",
1873
- "patternUnits",
1874
- "pointsAtX",
1875
- "pointsAtY",
1876
- "pointsAtZ",
1877
- "preserveAlpha",
1878
- "preserveAspectRatio",
1879
- "primitiveUnits",
1880
- "refX",
1881
- "refY",
1882
- "repeatCount",
1883
- "repeatDur",
1884
- "requiredExtensions",
1885
- "requiredFeatures",
1886
- "specularConstant",
1887
- "specularExponent",
1888
- "spreadMethod",
1889
- "startOffset",
1890
- "stdDeviation",
1891
- "stitchTiles",
1892
- "surfaceScale",
1893
- "systemLanguage",
1894
- "tableValues",
1895
- "targetX",
1896
- "targetY",
1897
- "textLength",
1898
- "viewBox",
1899
- "viewTarget",
1900
- "xChannelSelector",
1901
- "yChannelSelector",
1902
- "zoomAndPan"
1903
- ]);
1904
1997
  function spreadAttrs(obj) {
1905
1998
  if (!obj || typeof obj !== "object")
1906
1999
  return "";
@@ -1908,18 +2001,20 @@ function spreadAttrs(obj) {
1908
2001
  for (const [key, value] of Object.entries(obj)) {
1909
2002
  if (value == null || value === false)
1910
2003
  continue;
1911
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
1912
- continue;
1913
- if (key === "children")
2004
+ const c = classifyDOMProp(key);
2005
+ if (c.kind === "event" || c.kind === "skip" || c.kind === "ref")
1914
2006
  continue;
1915
- if (key === "style") {
2007
+ if (c.kind === "style") {
1916
2008
  const css = styleToCss(value);
1917
2009
  if (css != null)
1918
2010
  parts.push(`style="${css}"`);
1919
2011
  continue;
1920
2012
  }
1921
- const attr = key === "className" ? "class" : key === "htmlFor" ? "for" : SVG_CAMEL_CASE_ATTRS.has(key) ? key : key.replace(/([A-Z])/g, "-$1").toLowerCase();
1922
- parts.push(value === true ? attr : `${attr}="${value}"`);
2013
+ if (c.kind === "boolean" && value === true) {
2014
+ parts.push(c.attrName);
2015
+ } else {
2016
+ parts.push(`${c.attrName}="${value}"`);
2017
+ }
1923
2018
  }
1924
2019
  return parts.join(" ");
1925
2020
  }
@@ -2181,17 +2276,30 @@ function render(container, nameOrDef, props = {}) {
2181
2276
  }
2182
2277
  const tpl = document.createElement("template");
2183
2278
  tpl.innerHTML = html;
2184
- const element = tpl.content.firstChild;
2185
- if (!element) {
2279
+ const rootElements = Array.from(tpl.content.childNodes).filter((n) => n.nodeType === Node.ELEMENT_NODE);
2280
+ if (rootElements.length === 0) {
2186
2281
  throw new Error("[BarefootJS] render(): template returned empty HTML");
2187
2282
  }
2188
- if (!element.getAttribute(BF_SCOPE)) {
2189
- element.setAttribute(BF_SCOPE, scopeId);
2190
- }
2191
2283
  container.innerHTML = "";
2192
- container.appendChild(element);
2193
- init(element, props);
2194
- hydratedScopes.add(element);
2284
+ if (rootElements.length === 1) {
2285
+ const element = rootElements[0];
2286
+ if (!element.getAttribute(BF_SCOPE)) {
2287
+ element.setAttribute(BF_SCOPE, scopeId);
2288
+ }
2289
+ container.appendChild(element);
2290
+ init(element, props);
2291
+ hydratedScopes.add(element);
2292
+ return;
2293
+ }
2294
+ const commentNode = document.createComment(`${BF_SCOPE_COMMENT_PREFIX}${scopeId}`);
2295
+ container.appendChild(commentNode);
2296
+ for (const node of Array.from(tpl.content.childNodes)) {
2297
+ container.appendChild(node);
2298
+ }
2299
+ const proxyEl = rootElements[0];
2300
+ commentScopeRegistry.set(proxyEl, { commentNode, scopeId });
2301
+ init(proxyEl, props);
2302
+ hydratedScopes.add(proxyEl);
2195
2303
  }
2196
2304
  // src/runtime/streaming.ts
2197
2305
  function __bf_swap(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "BarefootJS client package: reactive primitives (SSR-safe) plus browser runtime under the `/runtime` subpath (compiler target)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,10 +55,10 @@
55
55
  "directory": "packages/client"
56
56
  },
57
57
  "dependencies": {
58
- "@barefootjs/shared": "0.1.3"
58
+ "@barefootjs/shared": "0.3.0"
59
59
  },
60
60
  "peerDependencies": {
61
- "@barefootjs/jsx": "0.1.3"
61
+ "@barefootjs/jsx": ">=0.2.0"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "@barefootjs/jsx": {
@@ -66,7 +66,6 @@
66
66
  }
67
67
  },
68
68
  "devDependencies": {
69
- "@barefootjs/jsx": "0.1.3",
70
69
  "@happy-dom/global-registrator": "^20.0.11",
71
70
  "typescript": "^5.0.0"
72
71
  }
@@ -6,16 +6,9 @@
6
6
  */
7
7
 
8
8
  import { createEffect } from '@barefootjs/client/reactive'
9
+ import { classifyDOMProp, type DOMPropClassification } from '@barefootjs/shared'
9
10
  import { styleToCss } from './style'
10
11
 
11
- /** Map of JSX prop names to HTML attribute names */
12
- function toAttrName(key: string): string {
13
- if (key === 'className') return 'class'
14
- if (key === 'htmlFor') return 'for'
15
- // Convert camelCase to kebab-case for data-* and aria-* style attributes
16
- return key.replace(/([A-Z])/g, '-$1').toLowerCase()
17
- }
18
-
19
12
  /**
20
13
  * Convert a JSX event prop name to a DOM event name for addEventListener.
21
14
  * Handles: camelCase → lowercase, plus special mappings (doubleclick → dblclick).
@@ -23,7 +16,6 @@ function toAttrName(key: string): string {
23
16
  */
24
17
  const jsxToDomEventMap: Record<string, string> = { doubleclick: 'dblclick' }
25
18
  function toEventName(jsxPropName: string): string {
26
- // onKeyDown → 'k' + 'eyDown' → 'keydown'
27
19
  const raw = (jsxPropName[2].toLowerCase() + jsxPropName.slice(3)).toLowerCase()
28
20
  return jsxToDomEventMap[raw] ?? raw
29
21
  }
@@ -43,15 +35,19 @@ export function applyRestAttrs(
43
35
  ): void {
44
36
  const exclude = new Set(excludeKeys)
45
37
 
46
- // Wire up event handlers and ref callbacks once (not reactively)
38
+ // Precompute classifications once keys are stable for rest props.
39
+ const classified: Array<{ key: string; c: DOMPropClassification }> = []
47
40
  for (const key of Object.keys(source)) {
48
41
  if (exclude.has(key)) continue
49
- if (key === 'ref') {
42
+ classified.push({ key, c: classifyDOMProp(key) })
43
+ }
44
+
45
+ // Wire up event handlers and ref callbacks once (not reactively)
46
+ for (const { key, c } of classified) {
47
+ if (c.kind === 'ref') {
50
48
  const ref = source[key]
51
49
  if (typeof ref === 'function') (ref as (el: Element) => void)(el)
52
- continue
53
- }
54
- if (key.startsWith('on') && key.length > 2 && key[2] === key[2].toUpperCase()) {
50
+ } else if (c.kind === 'event') {
55
51
  const handler = source[key]
56
52
  if (typeof handler === 'function') {
57
53
  el.addEventListener(toEventName(key), handler as EventListener)
@@ -59,49 +55,39 @@ export function applyRestAttrs(
59
55
  }
60
56
  }
61
57
 
62
- createEffect(() => {
63
- for (const key of Object.keys(source)) {
64
- if (exclude.has(key)) continue
65
-
66
- // Event handlers and ref are wired up above, not as attributes
67
- if (key === 'ref') continue
68
- // `children` is a JSX construct rendered inside the element, never
69
- // a DOM attribute. Without this exclusion, parent components that
70
- // pass `children` through `{...props}` end up with
71
- // `children="<p ...>...</p>"` written as a literal attribute on
72
- // the wrapper div. The matching `spreadAttrs` (SSR-string) path
73
- // already skips `children` for the same reason.
74
- if (key === 'children') continue
75
- if (key.startsWith('on') && key.length > 2 && key[2] === key[2].toUpperCase()) continue
58
+ // Filter to only attr-like entries for the reactive loop
59
+ const attrEntries = classified.filter(
60
+ ({ c }) => c.kind !== 'ref' && c.kind !== 'event' && c.kind !== 'skip',
61
+ )
76
62
 
63
+ createEffect(() => {
64
+ for (const { key, c } of attrEntries) {
77
65
  const value = source[key]
78
- const attr = toAttrName(key)
79
66
 
80
67
  if (value != null && value !== false) {
81
- // Use DOM property for value/checked (setAttribute sets the default, not current)
82
- if (attr === 'value' && 'value' in el) {
68
+ if (c.kind === 'property' && c.attrName === 'value' && 'value' in el) {
83
69
  const strVal = String(value)
84
70
  if ((el as HTMLInputElement).value !== strVal) (el as HTMLInputElement).value = strVal
85
- } else if (attr === 'checked' && 'checked' in el) {
71
+ } else if (c.kind === 'property' && c.attrName === 'checked' && 'checked' in el) {
86
72
  (el as HTMLInputElement).checked = !!value
87
- } else if (attr === 'style') {
88
- // Route the `style` prop through `styleToCss` so object literals
89
- // (`{'--err': errorHue()}`) and inline strings (`'color:red'`)
90
- // both reach the DOM as a real CSS string instead of
91
- // `[object Object]`. Mirrors the compiler's
92
- // `setAttribute('style', styleToCss(...))` path used when the
93
- // attribute is bound directly on a JSX element.
73
+ } else if (c.kind === 'boolean') {
74
+ el.setAttribute(c.attrName, '')
75
+ } else if (c.kind === 'style') {
94
76
  const css = styleToCss(value)
95
77
  if (css == null) el.removeAttribute('style')
96
78
  else el.setAttribute('style', css)
97
79
  } else {
98
- el.setAttribute(attr, String(value))
80
+ el.setAttribute(c.attrName, String(value))
99
81
  }
100
82
  } else {
101
- if (attr === 'checked' && 'checked' in el) {
83
+ if (c.kind === 'property' && c.attrName === 'value' && 'value' in el) {
84
+ (el as HTMLInputElement).value = ''
85
+ } else if (c.kind === 'property' && c.attrName === 'checked' && 'checked' in el) {
102
86
  (el as HTMLInputElement).checked = false
87
+ } else if (c.kind === 'boolean') {
88
+ el.removeAttribute(c.attrName)
103
89
  } else {
104
- el.removeAttribute(attr)
90
+ el.removeAttribute(c.attrName)
105
91
  }
106
92
  }
107
93
  }
@@ -124,13 +124,31 @@ export function createComponent(
124
124
  return result
125
125
  })
126
126
 
127
- // 4. Generate HTML from props.
127
+ // 4. Pre-generate the component's scope ID.
128
128
  //
129
- // Thread `slot.parent` into `_parentScopeId` so any hoisted-children
130
- // placeholder (#1320) resolves to the calling site's scope.
129
+ // `comment: true` components (synthesized inline-JSX-callback wrappers
130
+ // from #1211) render as transparent shells — the parsed `firstChild` is
131
+ // already the inner component's root with its own bf-s. Don't overwrite
132
+ // it (scopeId stays null), or `$c(__scope, 's0')` from the wrapper's
133
+ // init resolves to null.
134
+ const def = getRegisteredDef(name)
135
+ const isCommentWrapper = def?.comment === true
136
+ const scopeId = isCommentWrapper ? null : `${name}_${generateId()}`
137
+
138
+ // 5. Generate HTML from props.
139
+ //
140
+ // Thread the component's own scope ID into `_parentScopeId` for the
141
+ // template eval so renderChild() stamps parent-prefixed bf-s / bf-h /
142
+ // bf-m on child components — matching the SSR convention so a later
143
+ // `$c(scope, 'sN')` lookup resolves them. Without this, CSR-created
144
+ // children carry a random prefix and their event handlers never wire
145
+ // up (#1627). `slot.parent` takes precedence so hoisted-children
146
+ // placeholders (#1320) still resolve to the calling site's scope.
131
147
  const prevParentScopeId = _parentScopeId
132
148
  if (slot?.parent) {
133
149
  _parentScopeId = slot.parent
150
+ } else if (scopeId) {
151
+ _parentScopeId = scopeId
134
152
  }
135
153
  let html: string
136
154
  try {
@@ -139,7 +157,7 @@ export function createComponent(
139
157
  _parentScopeId = prevParentScopeId
140
158
  }
141
159
 
142
- // 5. Create DOM element
160
+ // 6. Create DOM element
143
161
  const element = parseHTML(html.trim()).firstChild as HTMLElement
144
162
 
145
163
  if (!element) {
@@ -147,16 +165,9 @@ export function createComponent(
147
165
  return createPlaceholder(name, key)
148
166
  }
149
167
 
150
- // 6. Set scope ID and key attributes.
151
- //
152
- // `comment: true` components (synthesized inline-JSX-callback wrappers
153
- // from #1211) render as transparent shells — the parsed `firstChild` is
154
- // already the inner component's root with its own bf-s. Don't overwrite
155
- // it, or `$c(__scope, 's0')` from the wrapper's init resolves to null.
156
- const def = getRegisteredDef(name)
157
- const isCommentWrapper = def?.comment === true
158
- if (!isCommentWrapper) {
159
- element.setAttribute(BF_SCOPE, `${name}_${generateId()}`)
168
+ // 7. Set scope ID and key attributes.
169
+ if (scopeId) {
170
+ element.setAttribute(BF_SCOPE, scopeId)
160
171
  }
161
172
  if (slot) {
162
173
  if (slot.parent) element.setAttribute(BF_HOST, slot.parent)
@@ -166,18 +177,18 @@ export function createComponent(
166
177
  element.setAttribute(BF_KEY, String(key))
167
178
  }
168
179
 
169
- // 7. Set currentScope so provideContext/useContext are element-scoped.
180
+ // 8. Set currentScope so provideContext/useContext are element-scoped.
170
181
  // This allows context providers in initFn to store context on this element.
171
182
  const prevScope = setCurrentScope(element)
172
183
 
173
- // 8. Initialize the component (context providers set up here).
184
+ // 9. Initialize the component (context providers set up here).
174
185
  const initFn = getComponentInit(name)
175
186
  if (initFn) {
176
187
  // Pass original props (with getters) for reactivity
177
188
  initFn(element, props)
178
189
  }
179
190
 
180
- // 9. Evaluate getter children and insert them.
191
+ // 10. Evaluate getter children and insert them.
181
192
  // Children are evaluated NOW (after initFn) so that context provided by
182
193
  // the parent is in the global store when children call useContext().
183
194
  if (childrenIsGetter) {
@@ -187,13 +198,13 @@ export function createComponent(
187
198
  }
188
199
  }
189
200
 
190
- // 10. Restore previous scope
201
+ // 11. Restore previous scope
191
202
  setCurrentScope(prevScope)
192
203
 
193
- // 11. Mark element as initialized
204
+ // 12. Mark element as initialized
194
205
  hydratedScopes.add(element)
195
206
 
196
- // 12. Store props and register update function for element reuse in reconcileList
207
+ // 13. Store props and register update function for element reuse in reconcileList
197
208
  propsMap.set(element, props)
198
209
  registerPropsUpdate(element, name, props)
199
210