@kudzujs/core 0.8.6 → 0.8.8

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/README.md CHANGED
@@ -10,7 +10,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
10
10
 
11
11
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
12
12
 
13
- **Latest release: 0.8.6 - Responsive list reversals.** Keyed-list reverse and removal fast paths now retain current bookkeeping, so repeated actions update immediately while preserving DOM identity. Read the [release notes](./RELEASES.md#086---responsive-list-reversals) or open the [release page](https://kudzujs.cloud/releases/0.8.6).
13
+ **Latest release: 0.8.8 - Conditional keyed map roots.** Expression-bodied keyed maps may return `condition && <Row />` or `condition ? <Row /> : null`; Kudzu lowers the condition to its existing filter path so omitted rows clean up and re-entry starts fresh. Read the [release notes](./RELEASES.md#088---conditional-keyed-map-roots) or open the [release page](https://kudzujs.cloud/releases/0.8.8).
14
14
 
15
15
  - [Documentation](https://kudzujs.cloud/docs)
16
16
  - [Installation guide](https://kudzujs.cloud/docs#install)
package/RELEASES.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.8 - Conditional keyed map roots
4
+
5
+ Kudzu 0.8.8 compiles ordinary expression-bodied conditional keyed maps through the existing pure collection selector path.
6
+
7
+ ### New in 0.8.8
8
+
9
+ - One-parameter keyed maps may return `condition && <Row />` or `condition ? <Row /> : null`.
10
+ - Top-level conditions may combine the current item with direct primitive parent state; nested maps support item-only conditions.
11
+ - Omitted rows own no DOM or hooks. True-to-false transitions release row state, effects, and refs; re-entry creates fresh ownership.
12
+ - Retained siblings preserve keyed DOM identity through condition changes, insertion, and reorder.
13
+ - Same-file and relative row components remain compiler-specialized before the existing list runtime receives the normalized filter.
14
+ - Imported build-known item-only conditions still fold to complete zero-JavaScript HTML.
15
+ - The React Notes migration restores its ordinary `notes.map(note => activeId === note.id && <Editor />)` source shape.
16
+ - Map indexes, alternate JSX fallbacks, block-bodied conditional maps, arbitrary captures, and impure predicates remain diagnosed.
17
+ - The complete suite passes 134/134 tests.
18
+
19
+ ### Boundary
20
+
21
+ Conditional map callbacks must be synchronous expression arrows with exactly one item parameter. Indexes are rejected because implicit filtering changes their meaning. The feature adds no runtime capability, VDOM, hydration, or component rerenderer.
22
+
23
+ ### Upgrade
24
+
25
+ ```bash
26
+ npm install @kudzujs/core@^0.8.8
27
+ ```
28
+
29
+ ## 0.8.7 - Reactive keyed row selection
30
+
31
+ Kudzu 0.8.7 lets a flat keyed row combine its current item or index with direct primitive parent state in pure text and attribute expressions.
32
+
33
+ ### New in 0.8.7
34
+
35
+ - Selected-row classes, `aria-current`, `aria-selected`, and similar pure expressions update directly after parent-state commits.
36
+ - Retained rows reevaluate only their expression text and attributes; list structure, handlers, and DOM identity remain untouched.
37
+ - Initial static HTML evaluates the same expression with build-time state values.
38
+ - Calculated SVG coverage verifies focus, Space, click, insertion, reorder, latest handlers, selected classes, ARIA state, and retained node identity.
39
+ - A React Notes migration restores its ordinary `activeId === note.id` selected-row source without imperative DOM code.
40
+ - Object/array parent state, arbitrary captures, nested-row parent state, structural conditions, mutation, and arbitrary calls remain diagnosed.
41
+ - The complete suite passes 134/134 tests.
42
+
43
+ ### Boundary
44
+
45
+ This support is limited to pure flat keyed-row text and attribute expressions over the current item/index plus direct primitive parent state. It adds no component rerenderer, VDOM, hydration, or general capture runtime.
46
+
47
+ ### Upgrade
48
+
49
+ ```bash
50
+ npm install @kudzujs/core@^0.8.7
51
+ ```
52
+
3
53
  ## 0.8.6 - Responsive list reversals
4
54
 
5
55
  Kudzu 0.8.6 fixes stale keyed-list bookkeeping after reverse and one-item removal fast paths.
@@ -303,6 +303,7 @@ export async function build({ quiet = false, minify = true } = {}) {
303
303
  .replace("function fillListItem(root, item, nested = false, index = 0)", "function fillListItem(root, item, nested = false)")
304
304
  .replace("fillListParts(root, parts, item, revision, index, previous)", "fillListParts(root, parts, item, revision, previous)")
305
305
  .replace("function fillListParts(root, parts, item, revision, index = 0, previous)", "function fillListParts(root, parts, item, revision, previous)")
306
+ .replace("fillListExpressions(root, parts, item, revision, index)", "fillListExpressions(root, parts, item, revision)")
306
307
  .replaceAll('value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value', 'value?.type === "list-item" ? serializeItem(item) : value')
307
308
  .replaceAll("evaluate(descriptor, item, index)", "evaluate(descriptor, item)")
308
309
  .replaceAll("evaluate({ module, handler }, item, index)", "evaluate({ module, handler }, item)")
@@ -311,6 +312,7 @@ export async function build({ quiet = false, minify = true } = {}) {
311
312
  .replace("fillListParts(marker, listItemParts(fragment), item, revision, index)", "fillListParts(marker, listItemParts(fragment), item, revision)")
312
313
  .replace("function evaluate(descriptor, item, index)", "function evaluate(descriptor, item)")
313
314
  .replace("exports[descriptor.handler](item, index)", "exports[descriptor.handler](item)")
315
+ .replace("exports[descriptor.handler](item, index, {", "exports[descriptor.handler](item, undefined, {")
314
316
  if (!hasCollectionSelectors) listRuntime = listRuntime.replaceAll(" && !list.descriptor.selector", "")
315
317
  if (!hasListIndexes) listRuntime = listRuntime
316
318
  .replaceAll(" && !list.descriptor.indexed", "")
@@ -3899,11 +3901,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
3899
3901
  }
3900
3902
 
3901
3903
  if (ts.isJsxExpression(node) && node.expression && listValues.has(node.expression)) {
3902
- return factory.updateJsxExpression(node, compileListValue(node.expression, listValues.get(node.expression), factory, listExpressions, handlerUrl))
3904
+ return factory.updateJsxExpression(node, compileListValue(node.expression, listValues.get(node.expression), factory, context, listExpressions, handlerUrl))
3903
3905
  }
3904
3906
 
3905
3907
  if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && listValues.has(node.initializer.expression)) {
3906
- return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compileListValue(node.initializer.expression, listValues.get(node.initializer.expression), factory, listExpressions, handlerUrl)))
3908
+ return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, compileListValue(node.initializer.expression, listValues.get(node.initializer.expression), factory, context, listExpressions, handlerUrl)))
3907
3909
  }
3908
3910
 
3909
3911
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
@@ -4123,7 +4125,7 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4123
4125
  const value = unwrapExpression(expression)
4124
4126
  const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
4125
4127
  if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
4126
- const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4128
+ let collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4127
4129
  if (!collection?.state && !collection?.calculation) return undefined
4128
4130
  if (directFrom) collection.selector.push(["from", undefined])
4129
4131
  let callback = directFrom ? value.arguments[1] : value.arguments[0]
@@ -4142,6 +4144,8 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4142
4144
  ts.setParentRecursive(callback, false)
4143
4145
  callback.parent = value
4144
4146
  }
4147
+ const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(setters.values()), factory, value)
4148
+ if (conditional) ({ callback, root, collection } = conditional)
4145
4149
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
4146
4150
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
4147
4151
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
@@ -4155,11 +4159,13 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4155
4159
  function nestedKeyedListParts(expression, parentItem, fail) {
4156
4160
  const value = unwrapExpression(expression)
4157
4161
  if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
4158
- const collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
4162
+ let collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
4159
4163
  if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
4160
- const callback = value.arguments[0]
4164
+ let callback = value.arguments[0]
4161
4165
  const parameters = collectionParameters(callback, "Nested keyed list map", fail)
4162
- const root = unwrapExpression(callback.body)
4166
+ let root = unwrapExpression(callback.body)
4167
+ const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(), ts.factory, value)
4168
+ if (conditional) ({ callback, root, collection } = conditional)
4163
4169
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Nested keyed list map callback must return one JSX element")
4164
4170
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
4165
4171
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
@@ -4170,6 +4176,28 @@ function nestedKeyedListParts(expression, parentItem, fail) {
4170
4176
  return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
4171
4177
  }
4172
4178
 
4179
+ function conditionalKeyedMapRoot(callback, root, parameters, collection, fail, stateNames, factory, parent) {
4180
+ let condition
4181
+ let rendered
4182
+ if (ts.isBinaryExpression(root) && root.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && (ts.isJsxElement(unwrapExpression(root.right)) || ts.isJsxSelfClosingElement(unwrapExpression(root.right)))) {
4183
+ condition = root.left
4184
+ rendered = unwrapExpression(root.right)
4185
+ } else if (ts.isConditionalExpression(root) && (ts.isJsxElement(unwrapExpression(root.whenTrue)) || ts.isJsxSelfClosingElement(unwrapExpression(root.whenTrue)))) {
4186
+ if (unwrapExpression(root.whenFalse).kind !== ts.SyntaxKind.NullKeyword) fail(root.whenFalse, "Conditional keyed map callbacks require condition ? <Element> : null")
4187
+ condition = root.condition
4188
+ rendered = unwrapExpression(root.whenTrue)
4189
+ } else {
4190
+ return undefined
4191
+ }
4192
+ if (parameters.index) fail(callback.parameters[1], "Conditional keyed map callbacks cannot use a map index because filtering changes index semantics; use an explicit filter(...).map((item, index) => ...) when a filtered index is intended")
4193
+ const selectorStates = new Set(collection.selectorStates)
4194
+ const selector = collectionExpression(condition, parameters, fail, stateNames, selectorStates)
4195
+ const normalized = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, rendered)
4196
+ ts.setParentRecursive(normalized, false)
4197
+ normalized.parent = parent
4198
+ return { callback: normalized, root: rendered, collection: { ...collection, selector: [...collection.selector, ["filter", selector]], selectorStates } }
4199
+ }
4200
+
4173
4201
  function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set(), importedCollectionTransforms = new Map(), factory = ts.factory, context, calculatedCollection, staticCollection) {
4174
4202
  const value = unwrapExpression(expression)
4175
4203
  if (ts.isIdentifier(value)) {
@@ -4509,9 +4537,12 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
4509
4537
  return
4510
4538
  }
4511
4539
  if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
4512
- validateListExpression(expression, item, node, fail, parts.index)
4540
+ const states = referencedStateNames(expression, setters)
4541
+ for (const rowState of rowStates) states.delete(rowState.state)
4542
+ if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
4543
+ validateListExpression(expression, item, node, fail, parts.index, states)
4513
4544
  if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
4514
- listValues.set(node.expression, { item, index: parts.index })
4545
+ listValues.set(node.expression, { item, index: parts.index, states })
4515
4546
  return
4516
4547
  }
4517
4548
  }
@@ -4983,7 +5014,7 @@ const assignmentOperators = new Set([
4983
5014
  ts.SyntaxKind.QuestionQuestionEqualsToken
4984
5015
  ])
4985
5016
 
4986
- function validateListExpression(expression, item, source, fail, index) {
5017
+ function validateListExpression(expression, item, source, fail, index, states = new Set()) {
4987
5018
  const visit = node => {
4988
5019
  if (ts.isTypeNode(node)) return
4989
5020
  if (ts.isElementAccessExpression(node) && referencesIdentifier(node.expression, item)) {
@@ -5014,7 +5045,7 @@ function validateListExpression(expression, item, source, fail, index) {
5014
5045
  fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
5015
5046
  }
5016
5047
  }
5017
- if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !pureListGlobals.has(node.text)) {
5048
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !states.has(node.text) && !pureListGlobals.has(node.text)) {
5018
5049
  fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
5019
5050
  }
5020
5051
  ts.forEachChild(node, visit)
@@ -5032,10 +5063,12 @@ function containsJsx(root) {
5032
5063
  return found
5033
5064
  }
5034
5065
 
5035
- function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl, index) {
5066
+ function compileListExpression(read, expression, item, factory, listExpressions, handlerUrl, index, states = new Set()) {
5036
5067
  const exportName = `listExpression${listExpressions.length}`
5037
- listExpressions.push({ exportName, expression, item, index })
5038
- return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)])
5068
+ listExpressions.push({ exportName, expression, item, index, states })
5069
+ const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
5070
+ if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
5071
+ return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
5039
5072
  }
5040
5073
 
5041
5074
  function compileListConditional(entry, factory, listExpressions, handlerUrl) {
@@ -5048,11 +5081,17 @@ function compileListConditional(entry, factory, listExpressions, handlerUrl) {
5048
5081
  ])
5049
5082
  }
5050
5083
 
5051
- function compileListValue(expression, entry, factory, listExpressions, handlerUrl) {
5052
- const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), expression)
5084
+ function compileListValue(expression, entry, factory, context, listExpressions, handlerUrl) {
5085
+ const rewrite = node => {
5086
+ if (ts.isShorthandPropertyAssignment(node) && entry.states?.has(node.name.text)) return factory.createPropertyAssignment(node.name, factory.createPropertyAccessExpression(node.name, "value"))
5087
+ if (ts.isIdentifier(node) && entry.states?.has(node.text) && isReferenceIdentifier(node)) return factory.createPropertyAccessExpression(node, "value")
5088
+ return ts.visitEachChild(node, rewrite, context)
5089
+ }
5090
+ const initial = entry.states?.size ? ts.visitNode(expression, rewrite) : expression
5091
+ const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), initial)
5053
5092
  return entry.field
5054
5093
  ? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
5055
- : compileListExpression(read, expression, entry.item, factory, listExpressions, handlerUrl, entry.index)
5094
+ : compileListExpression(read, expression, entry.item, factory, listExpressions, handlerUrl, entry.index, entry.states)
5056
5095
  }
5057
5096
 
5058
5097
  function directProperty(expression, objectName) {
@@ -6307,17 +6346,35 @@ function printReactiveBinding({ exportName, expression, captures, states }) {
6307
6346
  }
6308
6347
  }
6309
6348
 
6310
- function printListExpression({ exportName, expression, item, index }) {
6349
+ function printListExpression({ exportName, expression, item, index, states = new Set() }) {
6350
+ const factory = ts.factory
6351
+ const transformer = context => root => {
6352
+ const visitor = node => {
6353
+ if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
6354
+ return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
6355
+ }
6356
+ if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
6357
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.text)])
6358
+ }
6359
+ return ts.visitEachChild(node, visitor, context)
6360
+ }
6361
+ return ts.visitNode(root, visitor)
6362
+ }
6363
+ const transformed = ts.transform(expression, [transformer])
6311
6364
  const declaration = ts.factory.createFunctionDeclaration(
6312
6365
  [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
6313
6366
  undefined,
6314
6367
  exportName,
6315
6368
  undefined,
6316
- [ts.factory.createParameterDeclaration(undefined, undefined, item), ts.factory.createParameterDeclaration(undefined, undefined, index ?? "__kIndex")],
6369
+ [ts.factory.createParameterDeclaration(undefined, undefined, item), ts.factory.createParameterDeclaration(undefined, undefined, index ?? "__kIndex"), ts.factory.createParameterDeclaration(undefined, undefined, "__k")],
6317
6370
  undefined,
6318
- ts.factory.createBlock([ts.factory.createReturnStatement(expression)], true)
6371
+ ts.factory.createBlock([ts.factory.createReturnStatement(transformed.transformed[0])], true)
6319
6372
  )
6320
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
6373
+ try {
6374
+ return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
6375
+ } finally {
6376
+ transformed.dispose()
6377
+ }
6321
6378
  }
6322
6379
 
6323
6380
  function scopeRead(factory, name) {
@@ -38,7 +38,7 @@ export function bindingValue(value: unknown): unknown
38
38
  export function conditional(kind: "and" | "ternary", value: unknown, truthy: () => unknown, falsy: () => unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
39
39
  export function list(items: unknown, keyField: string | null, render: (item: unknown, index: number) => unknown, ownerField?: string, selector?: unknown[], indexed?: boolean, selectorStates?: Array<[string, unknown]>, staticCollection?: boolean): unknown
40
40
  export function listField(read: () => unknown, field: string): unknown
41
- export function listExpression(read: () => unknown, module: string, handler: string): unknown
41
+ export function listExpression(read: () => unknown, module: string, handler: string, states?: Array<[string, unknown]>): unknown
42
42
  export function listItem(): unknown
43
43
  export function listIndex(): unknown
44
44
  export function listConditional(kind: "and" | "ternary", read: () => unknown, truthy: () => unknown, falsy: () => unknown, module: string, handler: string): unknown
@@ -324,11 +324,17 @@ export function listField(read, field) {
324
324
  return { [listFieldMarker]: true, field, value: renderContext?.listTemplate ? undefined : read() }
325
325
  }
326
326
 
327
- export function listExpression(read, module, handler) {
327
+ export function listExpression(read, module, handler, states = []) {
328
328
  renderContext?.handlerModules.add(module)
329
+ const stateMap = Object.fromEntries(states.map(([name, state]) => {
330
+ if (!state?.[signalMarker] || !validEffectDependency(state.value)) throw new Error(`Derived keyed list item expression state ${JSON.stringify(name)} must be primitive Kudzu state`)
331
+ return [name, state.id]
332
+ }))
333
+ const owner = renderContext?.listRoot ?? renderContext?.listRowRoot
334
+ if (owner && states.length) owner.descriptor.expressionStates = [...new Set([...(owner.descriptor.expressionStates ?? []), ...Object.values(stateMap)])]
329
335
  const value = renderContext?.listTemplate ? undefined : read()
330
336
  if (value && typeof value.then === "function") throw new Error("Derived keyed list item expressions must return synchronous values")
331
- return { [listExpressionMarker]: true, module, handler, value }
337
+ return { [listExpressionMarker]: true, module, handler, states: stateMap, value }
332
338
  }
333
339
 
334
340
  export function listItem() {
@@ -659,7 +665,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
659
665
  return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-text-end></template>`
660
666
  }
661
667
  if (node?.[listExpressionMarker]) {
662
- const descriptor = { module: node.module, handler: node.handler }
668
+ const descriptor = { module: node.module, handler: node.handler, ...(Object.keys(node.states).length ? { states: node.states } : {}) }
663
669
  const marker = renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch ? ` data-k-list-expression='${escapeJsonAttribute(descriptor)}'` : ""
664
670
  return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-expression-end></template>`
665
671
  }
@@ -812,7 +818,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
812
818
  }
813
819
  if (value?.[listExpressionMarker]) {
814
820
  attributes += renderAttribute(name, value.value)
815
- listExpressionAttributes.push([name, value.module, value.handler])
821
+ listExpressionAttributes.push([name, value.module, value.handler, ...(Object.keys(value.states).length ? [value.states] : [])])
816
822
  if (name === "style") renderContext.hasListStyles = true
817
823
  continue
818
824
  }
@@ -25,7 +25,10 @@ function commitLists(id) {
25
25
  if (!lists) return
26
26
  for (const list of lists) {
27
27
  if (!list.start.isConnected) unregisterList(list.start)
28
- else updateList(list)
28
+ else {
29
+ if (list.updateStates.has(id)) updateList(list)
30
+ if (list.expressionStates.has(id)) updateListExpressions(list)
31
+ }
29
32
  }
30
33
  }
31
34
 
@@ -76,6 +79,8 @@ function mountLists(root) {
76
79
  ...(__KUDZU_LIST_STABLE_FAST_PATHS__ ? { orderedRoots: roots } : {}),
77
80
  values: new Map(),
78
81
  items: undefined,
82
+ updateStates: new Set(),
83
+ expressionStates: new Set(descriptor.expressionStates ?? []),
79
84
  container: roots[0]?.parentNode,
80
85
  boundary: end,
81
86
  ...(__KUDZU_NESTED_LISTS__ && descriptor.ownerField ? { owner: nested.owner } : {})
@@ -93,20 +98,19 @@ function mountLists(root) {
93
98
  loadListEvaluator(descriptor.source).then(evaluator => {
94
99
  if (listLoads.get(start) !== load || !start.isConnected) return
95
100
  list.sourceEvaluator = evaluator
96
- const states = [...new Set([...evaluator.stateIds, ...(__KUDZU_COLLECTION_SELECTORS__ ? Object.values(descriptor.selectorStates ?? {}) : [])])]
101
+ const updateStates = [...new Set([...evaluator.stateIds, ...(__KUDZU_COLLECTION_SELECTORS__ ? Object.values(descriptor.selectorStates ?? {}) : [])])]
102
+ const states = [...new Set([...updateStates, ...list.expressionStates])]
103
+ list.updateStates = new Set(updateStates)
97
104
  for (const state of states) register(listTargets, state, list)
98
105
  listRegistrations.set(start, { states, list })
99
106
  updateList(list)
100
107
  }).catch(error => console.error(error))
101
108
  } else {
102
- if (__KUDZU_COLLECTION_SELECTORS__ && descriptor.selectorStates) {
103
- const states = [...new Set([descriptor.state, ...Object.values(descriptor.selectorStates)])]
104
- for (const state of states) register(listTargets, state, list)
105
- listRegistrations.set(start, { states, list })
106
- } else {
107
- register(listTargets, descriptor.state, list)
108
- listRegistrations.set(start, { state: descriptor.state, list })
109
- }
109
+ const updateStates = [descriptor.state, ...(__KUDZU_COLLECTION_SELECTORS__ ? Object.values(descriptor.selectorStates ?? {}) : [])]
110
+ const states = [...new Set([...updateStates, ...list.expressionStates])]
111
+ list.updateStates = new Set(updateStates)
112
+ for (const state of states) register(listTargets, state, list)
113
+ listRegistrations.set(start, { states, list })
110
114
  }
111
115
  if (!descriptor.source) updateList(list)
112
116
  }
@@ -125,7 +129,7 @@ function unregisterList(start) {
125
129
  if (lists?.get(registration.list.descriptor.id) === registration.list) lists.delete(registration.list.descriptor.id)
126
130
  if (!lists?.size) ownedLists.delete(registration.owner)
127
131
  } else {
128
- if ((__KUDZU_COLLECTION_SELECTORS__ || registration.list.descriptor.source) && registration.states) {
132
+ if (registration.states) {
129
133
  for (const state of registration.states) {
130
134
  const lists = listTargets.get(state)
131
135
  lists?.delete(registration.list)
@@ -647,6 +651,17 @@ function fillListParts(root, parts, item, revision, index = 0, previous) {
647
651
  }
648
652
  }
649
653
  }
654
+ fillListExpressions(root, parts, item, revision, index)
655
+ if (__KUDZU_LIST_CONDITIONS__) {
656
+ for (const [marker, descriptor] of parts.conditions) {
657
+ evaluate(descriptor, item, index).then(value => {
658
+ if (revisions.get(root) === revision && marker.isConnected) updateListCondition(marker, descriptor.kind, value, item, index)
659
+ }).catch(error => console.error(error))
660
+ }
661
+ }
662
+ }
663
+
664
+ function fillListExpressions(root, parts, item, revision, index) {
650
665
  if (__KUDZU_LIST_EXPRESSIONS__) {
651
666
  for (const [marker, descriptor] of parts.expressions) {
652
667
  evaluate(descriptor, item, index).then(value => {
@@ -656,19 +671,20 @@ function fillListParts(root, parts, item, revision, index = 0, previous) {
656
671
  }
657
672
  if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__) {
658
673
  for (const [node, attributes] of parts.expressionAttributes) {
659
- for (const [target, module, handler] of attributes) {
660
- evaluate({ module, handler }, item, index).then(value => {
674
+ for (const [target, module, handler, states] of attributes) {
675
+ evaluate({ module, handler, states }, item, index).then(value => {
661
676
  if (revisions.get(root) === revision && node.isConnected) patchBinding(node, target, value)
662
677
  }).catch(error => console.error(error))
663
678
  }
664
679
  }
665
680
  }
666
- if (__KUDZU_LIST_CONDITIONS__) {
667
- for (const [marker, descriptor] of parts.conditions) {
668
- evaluate(descriptor, item, index).then(value => {
669
- if (revisions.get(root) === revision && marker.isConnected) updateListCondition(marker, descriptor.kind, value, item, index)
670
- }).catch(error => console.error(error))
671
- }
681
+ }
682
+
683
+ function updateListExpressions(list) {
684
+ for (const root of list.roots.values()) {
685
+ const revision = (revisions.get(root) ?? 0) + 1
686
+ revisions.set(root, revision)
687
+ fillListExpressions(root, listItemParts(root, list.descriptor.nested), listItems.get(root), revision, __KUDZU_LIST_INDEXES__ ? listIndexes.get(root) ?? 0 : 0)
672
688
  }
673
689
  }
674
690
 
@@ -999,7 +1015,7 @@ function evaluate(descriptor, item, index) {
999
1015
  imports.set(descriptor.module, module)
1000
1016
  }
1001
1017
  return module.then(exports => {
1002
- const value = exports[descriptor.handler](item, index)
1018
+ const value = exports[descriptor.handler](item, index, { get: name => browserState.get(descriptor.states?.[name]) })
1003
1019
  if (value && typeof value.then === "function") throw new Error("Derived keyed list item expressions must return synchronous values")
1004
1020
  return value
1005
1021
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",