@barefootjs/jsx 0.32.0 → 0.33.1
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/adapters/dangerous-inner-html.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +22 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +29 -6
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +274 -80
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +8 -0
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +12 -0
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +21 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/query-href-lowering.d.ts.map +1 -1
- package/dist/ssr-seed-plan.d.ts.map +1 -1
- package/dist/static-literal.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +2 -4
- package/src/__tests__/child-components-in-map.test.ts +11 -3
- package/src/__tests__/client-js-generation.test.ts +37 -1
- package/src/__tests__/expression-parser.test.ts +43 -6
- package/src/__tests__/inline-jsx-callback.test.ts +55 -0
- package/src/__tests__/ir-jsx-props.test.ts +148 -0
- package/src/__tests__/issue-2705-branch-inner-loop-container.test.ts +91 -0
- package/src/__tests__/markup-prop-brand.test.ts +49 -0
- package/src/__tests__/nested-loop-conditional.test.ts +20 -11
- package/src/__tests__/return-through-local-var.test.ts +269 -0
- package/src/__tests__/serialize-parsed-expr.test.ts +17 -3
- package/src/__tests__/ssr-defaults.test.ts +41 -0
- package/src/__tests__/ssr-seed-plan.test.ts +12 -2
- package/src/adapters/dangerous-inner-html.ts +1 -0
- package/src/adapters/parsed-expr-emitter.ts +49 -3
- package/src/analyzer.ts +71 -0
- package/src/errors.ts +17 -1
- package/src/expression-parser.ts +200 -89
- package/src/index.ts +2 -2
- package/src/ir-to-client-js/collect-elements.ts +31 -20
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +19 -0
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +22 -3
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +12 -0
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +7 -1
- package/src/ir-to-client-js/element-refs.ts +8 -0
- package/src/ir-to-client-js/emit-reactive.ts +91 -23
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/ir-to-client-js/index.ts +4 -0
- package/src/ir-to-client-js/phases/provider-and-child-inits.ts +5 -1
- package/src/ir-to-client-js/types.ts +21 -0
- package/src/jsx-to-ir.ts +161 -9
- package/src/query-href-lowering.ts +4 -0
- package/src/rich-type-refusal.ts +1 -1
- package/src/ssr-defaults.ts +51 -2
- package/src/ssr-seed-plan.ts +20 -3
- package/src/static-literal.ts +15 -1
- package/src/to-locale-date-lowering.ts +4 -0
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
} from '../../utils.ts'
|
|
31
31
|
import { buildChildRefBindings, buildStaticChildRefBindings } from '../shared.ts'
|
|
32
32
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
33
|
+
import { buildLoopChildConditionalsPlan } from './build-loop-child-arm.ts'
|
|
34
|
+
import type { LoopChildConditionalPlan } from './loop-child-arm.ts'
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Mirror of the helper in `build-loop-child-arm.ts` — kept local to avoid
|
|
@@ -160,6 +162,7 @@ function buildReactiveEmit(
|
|
|
160
162
|
outerLoopParamBindings?: readonly LoopParamBinding[],
|
|
161
163
|
): InnerLoopReactiveEmit {
|
|
162
164
|
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
165
|
+
const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
|
|
163
166
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
164
167
|
const wrappedKey = inner.key
|
|
165
168
|
? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
|
|
@@ -249,6 +252,21 @@ function buildReactiveEmit(
|
|
|
249
252
|
|
|
250
253
|
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
|
|
251
254
|
|
|
255
|
+
// Per-item conditionals inside THIS loop's own row (#2706) — same
|
|
256
|
+
// insert()-parity treatment the top-level loop's row conditionals
|
|
257
|
+
// already get (`buildLoopReactiveEffectsPlan`), now extended to a
|
|
258
|
+
// NESTED loop's row too. `scopeVar` is the row's own element
|
|
259
|
+
// (`__innerEl<uidSuffix>`, matching `stringifyInnerLoops`'s emission),
|
|
260
|
+
// and `wrapBoth` matches every other per-item expression in this emit
|
|
261
|
+
// (outer accessor, then inner accessor).
|
|
262
|
+
const conditionals: LoopChildConditionalPlan[] = buildLoopChildConditionalsPlan({
|
|
263
|
+
conditionals: inner.bindings.conditionals,
|
|
264
|
+
scopeVar: `__innerEl${uidSuffix}`,
|
|
265
|
+
wrap: wrapBoth,
|
|
266
|
+
loopParam: inner.param,
|
|
267
|
+
loopParamBindings: inner.paramBindings,
|
|
268
|
+
})
|
|
269
|
+
|
|
252
270
|
return {
|
|
253
271
|
mode: 'reactive',
|
|
254
272
|
keyFn: loopKeyFn(inner),
|
|
@@ -261,6 +279,7 @@ function buildReactiveEmit(
|
|
|
261
279
|
events,
|
|
262
280
|
reactiveTexts,
|
|
263
281
|
reactiveAttrs,
|
|
282
|
+
conditionals,
|
|
264
283
|
childRefs,
|
|
265
284
|
}
|
|
266
285
|
}
|
|
@@ -190,6 +190,14 @@ export interface BuildBranchInnerLoopsArgs {
|
|
|
190
190
|
innerLoops: readonly NestedLoop[] | undefined
|
|
191
191
|
/** The variable expression naming the parent scope element (e.g. `__branchScope`). */
|
|
192
192
|
scopeVar: string
|
|
193
|
+
/**
|
|
194
|
+
* The enclosing conditional's own slot id — every call site of this
|
|
195
|
+
* builder originates from a conditional branch's arm, so this is always
|
|
196
|
+
* a real id. Used as the `containerExpr` fallback (`findCondContainer`,
|
|
197
|
+
* #2705) for an inner loop whose IR never got a `containerSlotId` of its
|
|
198
|
+
* own (its wrapper element sits outside the branch's IR subtree).
|
|
199
|
+
*/
|
|
200
|
+
condSlotId: string
|
|
193
201
|
/** Outer loop param identifier (the conditional's enclosing loop). */
|
|
194
202
|
outerLoopParam: string
|
|
195
203
|
/** Outer loop param destructuring metadata. */
|
|
@@ -214,6 +222,7 @@ export function buildBranchInnerLoopsPlan(
|
|
|
214
222
|
const {
|
|
215
223
|
innerLoops,
|
|
216
224
|
scopeVar,
|
|
225
|
+
condSlotId,
|
|
217
226
|
outerLoopParam,
|
|
218
227
|
outerLoopParamBindings,
|
|
219
228
|
wrapOuter,
|
|
@@ -230,10 +239,15 @@ export function buildBranchInnerLoopsPlan(
|
|
|
230
239
|
|
|
231
240
|
const csl = inner.containerSlotId
|
|
232
241
|
// Inner loop's container: host-side `bf="<slot>"` slot marker first,
|
|
233
|
-
// then (bf-h, bf-m) when the container is itself a child scope.
|
|
242
|
+
// then (bf-h, bf-m) when the container is itself a child scope. When
|
|
243
|
+
// neither exists — the loop's own IR never got a `containerSlotId`
|
|
244
|
+
// because its wrapper element sits outside the branch's IR subtree
|
|
245
|
+
// (#2705) — resolve via the conditional's OWN comment marker instead
|
|
246
|
+
// of falling back to the whole branch scope, which may be several
|
|
247
|
+
// elements wider than the loop's actual container.
|
|
234
248
|
const containerExpr = csl
|
|
235
249
|
? `(${scopeVar}.querySelector('[bf="${csl}"]') ?? ${scopeVar}.querySelector(\`[${BF_HOST}="\${__scopeId}"][${BF_AT}="${csl}"]\`) ?? ${scopeVar})`
|
|
236
|
-
: scopeVar
|
|
250
|
+
: `findCondContainer(${scopeVar}, '${condSlotId}')`
|
|
237
251
|
|
|
238
252
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
239
253
|
const wrappedKey = inner.key
|
|
@@ -372,12 +386,14 @@ export function buildLoopChildConditionalsPlan(
|
|
|
372
386
|
wrap,
|
|
373
387
|
loopParam,
|
|
374
388
|
loopParamBindings,
|
|
389
|
+
condId: cond.slotId,
|
|
375
390
|
}),
|
|
376
391
|
whenFalseArm: buildLoopChildArmPlan({
|
|
377
392
|
branch: cond.whenFalse,
|
|
378
393
|
wrap,
|
|
379
394
|
loopParam,
|
|
380
395
|
loopParamBindings,
|
|
396
|
+
condId: cond.slotId,
|
|
381
397
|
}),
|
|
382
398
|
})
|
|
383
399
|
}
|
|
@@ -439,10 +455,12 @@ interface BuildLoopChildArmArgs {
|
|
|
439
455
|
wrap: (expr: string) => string
|
|
440
456
|
loopParam: string
|
|
441
457
|
loopParamBindings?: readonly LoopParamBinding[]
|
|
458
|
+
/** The enclosing conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
|
|
459
|
+
condId: string
|
|
442
460
|
}
|
|
443
461
|
|
|
444
462
|
function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
445
|
-
const { branch, wrap, loopParam, loopParamBindings } = args
|
|
463
|
+
const { branch, wrap, loopParam, loopParamBindings, condId } = args
|
|
446
464
|
return {
|
|
447
465
|
events: buildBranchEventBindingsPlan({
|
|
448
466
|
events: branch.events,
|
|
@@ -455,6 +473,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
|
455
473
|
innerLoops: buildBranchInnerLoopsPlan({
|
|
456
474
|
innerLoops: branch.innerLoops,
|
|
457
475
|
scopeVar: '__branchScope',
|
|
476
|
+
condSlotId: condId,
|
|
458
477
|
outerLoopParam: loopParam,
|
|
459
478
|
outerLoopParamBindings: loopParamBindings,
|
|
460
479
|
wrapOuter: wrap,
|
|
@@ -110,8 +110,8 @@ export function buildReactiveEffectsPlan(
|
|
|
110
110
|
wrappedCondition: wrap(cond.condition),
|
|
111
111
|
whenTrueTemplateHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
|
|
112
112
|
whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
|
|
113
|
-
whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, profileComponentName),
|
|
114
|
-
whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, profileComponentName),
|
|
113
|
+
whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, cond.slotId, profileComponentName),
|
|
114
|
+
whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, cond.slotId, profileComponentName),
|
|
115
115
|
...(cond.readsPreamble && { readsPreamble: true }),
|
|
116
116
|
})
|
|
117
117
|
}
|
|
@@ -130,6 +130,8 @@ function buildOuterArm(
|
|
|
130
130
|
wrap: (expr: string) => string,
|
|
131
131
|
loopParam: string,
|
|
132
132
|
loopParamBindings: readonly LoopParamBinding[] | undefined,
|
|
133
|
+
/** The conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
|
|
134
|
+
condSlotId: string,
|
|
133
135
|
profileComponentName?: string,
|
|
134
136
|
): LoopChildArmPlan {
|
|
135
137
|
return {
|
|
@@ -145,6 +147,7 @@ function buildOuterArm(
|
|
|
145
147
|
innerLoops: buildBranchInnerLoopsPlan({
|
|
146
148
|
innerLoops: branch.innerLoops,
|
|
147
149
|
scopeVar: '__branchScope',
|
|
150
|
+
condSlotId,
|
|
148
151
|
outerLoopParam: loopParam,
|
|
149
152
|
outerLoopParamBindings: loopParamBindings,
|
|
150
153
|
wrapOuter: wrap,
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
LoopParamBinding,
|
|
18
18
|
} from '../../../types.ts'
|
|
19
19
|
import type { LoopChildRefBinding } from './loop.ts'
|
|
20
|
+
import type { LoopChildConditionalPlan } from './loop-child-arm.ts'
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Body-entry statements emitted in order at the top of a `mapArray`
|
|
@@ -148,6 +149,17 @@ export interface InnerLoopReactiveEmit {
|
|
|
148
149
|
reactiveTexts: readonly InnerLoopText[]
|
|
149
150
|
/** Pre-wrapped reactive attribute effects for the inner-item body. */
|
|
150
151
|
reactiveAttrs: readonly InnerLoopReactiveAttr[]
|
|
152
|
+
/**
|
|
153
|
+
* Per-item conditionals inside THIS loop's own row (#2706) — each gets
|
|
154
|
+
* its own `insert()` call, the same insert()-parity the top-level loop's
|
|
155
|
+
* row conditionals already have (`ReactiveEffectsPlan.conditionals`).
|
|
156
|
+
* Before this field existed, a per-item conditional inside a nested
|
|
157
|
+
* loop's row was baked into the static row template ONCE at row
|
|
158
|
+
* creation and never revisited — silently frozen against later signal
|
|
159
|
+
* changes — while its reactive text still (unsoundly) assumed insert()
|
|
160
|
+
* kept the branch's marker around.
|
|
161
|
+
*/
|
|
162
|
+
conditionals: readonly LoopChildConditionalPlan[]
|
|
151
163
|
/** Pre-wrapped imperative ref callbacks for the inner-item body (#1244). */
|
|
152
164
|
childRefs: readonly LoopChildRefBinding[]
|
|
153
165
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* <indent> emitComponentAndEventSetup(...)
|
|
15
15
|
* <indent> recurse on childLevels
|
|
16
16
|
* <indent> reactive text effects
|
|
17
|
+
* <indent> per-item conditionals: insert() over __innerEl<uid> (#2706)
|
|
17
18
|
* <indent> return __innerEl<uid>
|
|
18
19
|
* <indent>}) }
|
|
19
20
|
*
|
|
@@ -36,6 +37,7 @@ import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
|
36
37
|
import { emitMultiRootTemplateCloneLines, namespaceWrapForTemplate } from './template-parse.ts'
|
|
37
38
|
import { emitLoopChildRefs } from './loop.ts'
|
|
38
39
|
import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
|
|
40
|
+
import { stringifyLoopChildConditionals } from './loop-child-arm.ts'
|
|
39
41
|
import type {
|
|
40
42
|
InnerLoopPlan,
|
|
41
43
|
InnerLoopsPlan,
|
|
@@ -139,6 +141,13 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
|
|
|
139
141
|
}
|
|
140
142
|
lines.push(`${indent} }${profileBindingId(pc, attr.slotId)}) }`)
|
|
141
143
|
}
|
|
144
|
+
// Per-item conditionals inside THIS loop's own row (#2706) — each is a
|
|
145
|
+
// real `insert()` over `__innerEl<uid>`, not a bake-once-at-creation
|
|
146
|
+
// ternary. Mirrors `stringifyBranchInnerLoops`'s identical call for a
|
|
147
|
+
// branch-scoped inner loop's own conditionals.
|
|
148
|
+
if (emit.conditionals.length > 0) {
|
|
149
|
+
stringifyLoopChildConditionals(lines, emit.conditionals, `${indent} `, pc)
|
|
150
|
+
}
|
|
142
151
|
// Imperative ref callbacks fire on every renderItem invocation, which
|
|
143
152
|
// means every mount: SSR hydration, initial CSR creation, and same-key
|
|
144
153
|
// remount after unmount (#1244).
|
|
@@ -553,7 +553,13 @@ function seedDiffersExpr(target: string, a: LazyRowAttrBinding): string {
|
|
|
553
553
|
if (a.attrName === 'dangerouslySetInnerHTML' || html === 'dangerouslySetInnerHTML') return 'true'
|
|
554
554
|
if (html === 'style') return `${target}.getAttribute('style') !== styleToCss(__x)`
|
|
555
555
|
if (html === 'class') return `${target}.getAttribute('class') !== (__x != null ? String(__x) : null)`
|
|
556
|
-
|
|
556
|
+
// Mirrors `emitValueUpdateStatements`'s runtime `'value' in target` gate
|
|
557
|
+
// (#2716): a target with no native `.value` property never gets one
|
|
558
|
+
// written, so the seed-diff must compare against the ATTRIBUTE it would
|
|
559
|
+
// actually receive there, not the (absent) property.
|
|
560
|
+
if (html === 'value') {
|
|
561
|
+
return `('value' in ${target} ? ${target}.value !== String(__x) : ${target}.getAttribute('value') !== String(__x))`
|
|
562
|
+
}
|
|
557
563
|
if (isBooleanAttr(html)) return `${target}.${html} !== !!(__x)`
|
|
558
564
|
if (a.meta.presenceOrUndefined) {
|
|
559
565
|
// Compare the VALUE the writer would produce, not just presence.
|
|
@@ -71,6 +71,14 @@ export function generateElementRefs(ctx: ClientJsContext): string {
|
|
|
71
71
|
regularSlots.delete(slotId)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// The component's own `comment: true` root slot needs no `$c` ref at
|
|
75
|
+
// all — its consumers (provider-and-child-inits.ts, emit-reactive.ts)
|
|
76
|
+
// reference `__scope` directly instead (#2649, see
|
|
77
|
+
// `ClientJsContext.commentScopeRootSlotId`'s docstring).
|
|
78
|
+
if (ctx.commentScopeRootSlotId) {
|
|
79
|
+
componentSlots.delete(ctx.commentScopeRootSlotId)
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
if (regularSlots.size === 0 && componentSlots.size === 0) return ''
|
|
75
83
|
|
|
76
84
|
const refLines: string[] = []
|
|
@@ -28,6 +28,57 @@ function bindingIdArg(ctx: ClientJsContext, slotId: string | undefined): string
|
|
|
28
28
|
return `, ${JSON.stringify(`${ctx.componentName}#binding:${slotId}`)}`
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Generate statements that write a `value` HTML ATTRIBUTE the developer
|
|
33
|
+
* wrote directly on an element (`<div value={x}>`, a loop row's `<li
|
|
34
|
+
* value={x}>`, …) — SSR renders that same attribute, so hydration keeping
|
|
35
|
+
* it in sync is exactly the contract. Gated at runtime to elements that
|
|
36
|
+
* ALREADY expose a native `.value` IDL property (`'value' in target` —
|
|
37
|
+
* form controls, but also e.g. `<li value>`; the same duck-type check
|
|
38
|
+
* `applyRestAttrs` uses, deliberately not a tag-name allowlist), because
|
|
39
|
+
* for those `setAttribute('value', x)` only sets the INITIAL HTML
|
|
40
|
+
* attribute and the live property is required after user interaction; any
|
|
41
|
+
* other element falls back to a plain attribute write, which still matches
|
|
42
|
+
* what SSR rendered there. Writing the live property unconditionally would
|
|
43
|
+
* plant an expando SSR never had — a hydrated/SSR DOM-state divergence and a
|
|
44
|
+
* hazard for anything that duck-types form controls via `'value' in el`
|
|
45
|
+
* (#2716).
|
|
46
|
+
*
|
|
47
|
+
* NOT for the child-component-root `value` MIRROR (`emitReactivePropBindings`
|
|
48
|
+
* / `emitReactiveChildProps` reflecting a named prop onto a child's root
|
|
49
|
+
* element) — that mechanism has no SSR-rendered counterpart at all
|
|
50
|
+
* regardless of prop name, so an attribute fallback there would itself
|
|
51
|
+
* plant a fresh SSR/hydrate divergence; see `emitChildValueMirrorStatements`.
|
|
52
|
+
*/
|
|
53
|
+
function emitValueUpdateStatements(target: string, expression: string): string[] {
|
|
54
|
+
return [
|
|
55
|
+
`const __val = String(${expression})`,
|
|
56
|
+
`if ('value' in ${target}) { if (${target}.value !== __val) ${target}.value = __val } else { ${target}.setAttribute('value', __val) }`,
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `value`-prop write for the CHILD-ROOT MIRROR mechanism
|
|
62
|
+
* (`emitReactivePropBindings` / `emitReactiveChildProps`, both reactively
|
|
63
|
+
* reflect a parent-passed NAMED PROP onto a child component's root DOM
|
|
64
|
+
* element). Unlike a developer-authored `value=` attribute
|
|
65
|
+
* (`emitValueUpdateStatements`), this mirror has NO SSR-rendered
|
|
66
|
+
* counterpart at all — SSR never puts a `value` attribute on a child's
|
|
67
|
+
* root just because the parent passed a `value` prop. So a root WITHOUT a
|
|
68
|
+
* native `.value` property gets NOTHING written — not even an attribute
|
|
69
|
+
* (confirmed against the oracle's structural-HTML comparison, #2716: an
|
|
70
|
+
* attribute fallback here reintroduced a fresh SSR/hydrate divergence one
|
|
71
|
+
* layer down from the IDL-property expando this replaces). A root that
|
|
72
|
+
* already exposes `.value` (`'value' in target`, same duck-type gate as
|
|
73
|
+
* the direct-attribute case) still gets the live controlled-value
|
|
74
|
+
* property.
|
|
75
|
+
*/
|
|
76
|
+
function emitChildValueMirrorStatements(target: string, expression: string): string[] {
|
|
77
|
+
return [
|
|
78
|
+
`if ('value' in ${target}) { const __val = String(${expression}); if (${target}.value !== __val) ${target}.value = __val }`,
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
|
|
31
82
|
/**
|
|
32
83
|
* Generate JS statements to update a DOM attribute reactively.
|
|
33
84
|
* Centralizes the attribute-type dispatch (value, class, boolean, presence, generic)
|
|
@@ -55,10 +106,7 @@ export function emitAttrUpdate(target: string, attrName: string, expression: str
|
|
|
55
106
|
]
|
|
56
107
|
}
|
|
57
108
|
if (htmlName === 'value') {
|
|
58
|
-
return
|
|
59
|
-
`const __val = String(${expression})`,
|
|
60
|
-
`if (${target}.value !== __val) ${target}.value = __val`,
|
|
61
|
-
]
|
|
109
|
+
return emitValueUpdateStatements(target, expression)
|
|
62
110
|
}
|
|
63
111
|
if (isBooleanAttr(htmlName)) {
|
|
64
112
|
return [`${target}.${htmlName} = !!(${expression})`]
|
|
@@ -462,37 +510,43 @@ export function emitReactivePropBindings(lines: string[], ctx: ClientJsContext):
|
|
|
462
510
|
}
|
|
463
511
|
|
|
464
512
|
for (const [slotId, props] of propsBySlot) {
|
|
465
|
-
|
|
466
|
-
|
|
513
|
+
// The component's own `comment: true` root child IS `__scope` — no
|
|
514
|
+
// `$c` ref was declared for it (element-refs.ts), so reference
|
|
515
|
+
// `__scope` directly instead of a `_sN` var that doesn't exist
|
|
516
|
+
// (#2649, see `ClientJsContext.commentScopeRootSlotId`'s docstring).
|
|
517
|
+
const ref = slotId === ctx.commentScopeRootSlotId ? '__scope' : `_${varSlotId(slotId)}`
|
|
518
|
+
lines.push(` if (${ref}) {`)
|
|
467
519
|
for (const prop of props) {
|
|
468
520
|
const value = `${prop.expression}()`
|
|
469
521
|
if (prop.propName === 'selected') {
|
|
470
522
|
if (prop.componentName === 'TabsContent') {
|
|
471
|
-
lines.push(`
|
|
523
|
+
lines.push(` ${ref}.setAttribute('data-state', ${value} ? 'active' : 'inactive')`)
|
|
472
524
|
lines.push(` if (${value}) {`)
|
|
473
|
-
lines.push(`
|
|
525
|
+
lines.push(` ${ref}.classList.remove('hidden')`)
|
|
474
526
|
lines.push(` } else {`)
|
|
475
|
-
lines.push(`
|
|
527
|
+
lines.push(` ${ref}.classList.add('hidden')`)
|
|
476
528
|
lines.push(` }`)
|
|
477
529
|
} else {
|
|
478
530
|
// Update data-state and aria-selected attributes.
|
|
479
531
|
// Visual styling is driven by CSS data-[state=active/inactive]: selectors.
|
|
480
|
-
lines.push(`
|
|
481
|
-
lines.push(`
|
|
482
|
-
lines.push(`
|
|
532
|
+
lines.push(` ${ref}.setAttribute('aria-selected', String(${value}))`)
|
|
533
|
+
lines.push(` ${ref}.setAttribute('data-state', ${value} ? 'active' : 'inactive')`)
|
|
534
|
+
lines.push(` ${ref}.setAttribute('tabindex', ${value} ? '0' : '-1')`)
|
|
483
535
|
}
|
|
484
|
-
// Use DOM property assignment for value and boolean attrs
|
|
485
|
-
//
|
|
486
|
-
//
|
|
536
|
+
// Use DOM property assignment for value and boolean attrs, but only
|
|
537
|
+
// on genuine form controls — see `emitChildValueMirrorStatements`'s
|
|
538
|
+
// docstring (#2716). `ref` here is a named prop's MIRROR target
|
|
539
|
+
// element (a child component's arbitrary root), not necessarily a
|
|
540
|
+
// form control, and this mirror has no SSR-rendered counterpart to
|
|
541
|
+
// fall back to.
|
|
487
542
|
// Boolean attrs (disabled, checked, etc.) treat any attribute presence as
|
|
488
543
|
// truthy, so setAttribute('disabled', 'false') still disables the element.
|
|
489
544
|
} else if (prop.propName === 'value') {
|
|
490
|
-
lines.push(`
|
|
491
|
-
lines.push(` if (_${v}.value !== __val) _${v}.value = __val`)
|
|
545
|
+
for (const stmt of emitChildValueMirrorStatements(ref, value)) lines.push(` ${stmt}`)
|
|
492
546
|
} else if (isBooleanAttr(prop.propName)) {
|
|
493
|
-
lines.push(`
|
|
547
|
+
lines.push(` ${ref}.${prop.propName} = !!(${value})`)
|
|
494
548
|
} else {
|
|
495
|
-
lines.push(`
|
|
549
|
+
lines.push(` ${ref}.setAttribute('${prop.propName}', String(${value}))`)
|
|
496
550
|
}
|
|
497
551
|
}
|
|
498
552
|
lines.push(` }`)
|
|
@@ -520,13 +574,27 @@ export function emitReactiveChildProps(lines: string[], ctx: ClientJsContext): v
|
|
|
520
574
|
|
|
521
575
|
for (const [, props] of propsByComponent) {
|
|
522
576
|
const first = props[0]
|
|
577
|
+
// The component's own `comment: true` root child IS `__scope` — query
|
|
578
|
+
// it directly rather than through `$c`, which cannot tell "I already
|
|
579
|
+
// am this slot" apart from a coincidentally-matching descendant
|
|
580
|
+
// (#2649, see `ClientJsContext.commentScopeRootSlotId`'s docstring).
|
|
581
|
+
const isCommentRoot = first.slotId !== null && first.slotId === ctx.commentScopeRootSlotId
|
|
523
582
|
const varSuffix = first.slotId ? varSlotId(first.slotId).replace(/-/g, '_') : first.componentName
|
|
524
|
-
const varName = `__${first.componentName}_${varSuffix}El`
|
|
525
|
-
|
|
526
|
-
|
|
583
|
+
const varName = isCommentRoot ? '__scope' : `__${first.componentName}_${varSuffix}El`
|
|
584
|
+
if (!isCommentRoot) {
|
|
585
|
+
const selectorArg = first.slotId ? first.slotId : first.componentName
|
|
586
|
+
lines.push(` const [${varName}] = $c(__scope, '${selectorArg}')`)
|
|
587
|
+
}
|
|
527
588
|
lines.push(` if (${varName}) {`)
|
|
528
589
|
for (const prop of props) {
|
|
529
|
-
|
|
590
|
+
// `value` is the CHILD-ROOT MIRROR case, not a developer-authored
|
|
591
|
+
// attribute — route it through the no-SSR-fallback helper instead
|
|
592
|
+
// of `emitAttrUpdate`'s generic (attribute-fallback) dispatch;
|
|
593
|
+
// see `emitChildValueMirrorStatements`'s docstring (#2716).
|
|
594
|
+
const stmts = toHtmlAttrName(prop.attrName) === 'value'
|
|
595
|
+
? emitChildValueMirrorStatements(varName, prop.expression)
|
|
596
|
+
: emitAttrUpdate(varName, prop.attrName, prop.expression, prop)
|
|
597
|
+
for (const stmt of stmts) {
|
|
530
598
|
lines.push(` ${stmt}`)
|
|
531
599
|
}
|
|
532
600
|
}
|
|
@@ -11,6 +11,11 @@ import { identifierCallPattern } from '../identifier-pattern.ts'
|
|
|
11
11
|
export const RUNTIME_IMPORT_CANDIDATES = [
|
|
12
12
|
'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
|
|
13
13
|
'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect',
|
|
14
|
+
// Resolves the real DOM container for a loop nested inside a loop-row
|
|
15
|
+
// conditional's branch when the conditional's wrapper element carries no
|
|
16
|
+
// `bf="<slot>"` marker of its own (#2705) — see `findCondContainer`'s
|
|
17
|
+
// docstring (runtime/insert.ts) for why the marker collector can't see it.
|
|
18
|
+
'findCondContainer',
|
|
14
19
|
'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild',
|
|
15
20
|
// Connects a template-clone loop row before the body's tail runs, so a child
|
|
16
21
|
// that inits inside it resolves context against real ancestors rather than
|
|
@@ -185,6 +185,10 @@ function createContext(
|
|
|
185
185
|
refElements: [],
|
|
186
186
|
childInits: [],
|
|
187
187
|
deferredChildSlots: new Set(),
|
|
188
|
+
// Mirrors `emit-registration.ts`'s `isCommentScope` component-root
|
|
189
|
+
// disjunct (not the fragment disjunct — that case is disambiguated at
|
|
190
|
+
// runtime by `commentScopeRegistry`, not by this slot-id shortcut).
|
|
191
|
+
commentScopeRootSlotId: ir.root.type === 'component' ? ir.root.slotId : null,
|
|
188
192
|
reactiveProps: [],
|
|
189
193
|
reactiveChildProps: [],
|
|
190
194
|
reactiveAttrs: [],
|
|
@@ -39,7 +39,11 @@ export function emitProviderAndChildInits(lines: string[], ctx: ClientJsContext)
|
|
|
39
39
|
lines.push(` upsertChild(__scope, '${registryName}', '${child.slotId}', ${child.propsExpr})`)
|
|
40
40
|
continue
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
// The component's own `comment: true` root child IS `__scope` (no
|
|
43
|
+
// separate DOM node exists for it to be looked up at) — see
|
|
44
|
+
// `ClientJsContext.commentScopeRootSlotId`'s docstring (#2649).
|
|
45
|
+
const isCommentRoot = child.slotId !== null && child.slotId === ctx.commentScopeRootSlotId
|
|
46
|
+
const scopeRef = !child.slotId || isCommentRoot ? '__scope' : `_${varSlotId(child.slotId)}`
|
|
43
47
|
lines.push(` initChild('${registryName}', ${scopeRef}, ${child.propsExpr})`)
|
|
44
48
|
}
|
|
45
49
|
}
|
|
@@ -102,6 +102,27 @@ export interface ClientJsContext {
|
|
|
102
102
|
* registration-template emit so both agree on which children defer.
|
|
103
103
|
*/
|
|
104
104
|
deferredChildSlots: Set<string>
|
|
105
|
+
/**
|
|
106
|
+
* Slot id of the component's own root, when the ENTIRE render is a single
|
|
107
|
+
* child component call (`ir.root.type === 'component'` — the IR shape is
|
|
108
|
+
* the sole source of truth; `emit-registration.ts`'s `isCommentScope`
|
|
109
|
+
* derives the def's `comment: true` from the same shape). Such a child
|
|
110
|
+
* never gets its own DOM node: `materializeComponent`/`renderChild` leave
|
|
111
|
+
* `bf-s` unset and the child's markup becomes `__scope` itself (#2649).
|
|
112
|
+
* A generic `$c(__scope, slotId)` lookup for this one slot is therefore
|
|
113
|
+
* not just redundant but actively ambiguous — a genuine grandchild
|
|
114
|
+
* nested inside it can derive a `bf-s` whose suffix collides with this
|
|
115
|
+
* same slot id (see `component.ts`'s `_parentScopeId` push), and
|
|
116
|
+
* `$cSingle` cannot tell "I already am this slot" apart from "a
|
|
117
|
+
* coincidentally-matching descendant is this slot" from the DOM alone.
|
|
118
|
+
* Emission sites that would otherwise query this slot via `$c`
|
|
119
|
+
* (`element-refs.ts`, `provider-and-child-inits.ts`,
|
|
120
|
+
* `emit-reactive.ts`) use `__scope` directly instead, sidestepping the
|
|
121
|
+
* ambiguity entirely rather than trying to make the query precise
|
|
122
|
+
* enough to resolve it. `null` for a component whose root is a regular
|
|
123
|
+
* element/fragment (the overwhelmingly common case).
|
|
124
|
+
*/
|
|
125
|
+
commentScopeRootSlotId: string | null
|
|
105
126
|
reactiveProps: ReactiveComponentProp[]
|
|
106
127
|
reactiveChildProps: ReactiveChildProp[]
|
|
107
128
|
reactiveAttrs: ReactiveAttribute[]
|