@kudzujs/core 0.6.26 → 0.6.28

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.
@@ -0,0 +1,63 @@
1
+ export function selectCollection(anchor, selector = []) {
2
+ let values = anchor == null ? [] : anchor
3
+ for (const operation of selector) {
4
+ if (operation[0] === "from") values = Array.from(values, operation[1] ? (item, index) => evaluateCollectionExpression(operation[1], item, index) : undefined)
5
+ else {
6
+ if (!Array.isArray(values)) throw new Error("Rendered collection source must remain an array")
7
+ if (operation[0] === "filter") values = values.filter((item, index) => evaluateCollectionExpression(operation[1], item, index))
8
+ else if (operation[0] === "flatMap") values = values.flatMap(item => item?.[operation[1]] ?? [])
9
+ }
10
+ }
11
+ if (!Array.isArray(values)) throw new Error("Rendered collection source must remain an array")
12
+ return values
13
+ }
14
+
15
+ function evaluateCollectionExpression(expression, item, index) {
16
+ const [kind, ...parts] = expression
17
+ if (kind === "value") return parts[0]
18
+ if (kind === "undefined") return undefined
19
+ if (kind === "item") return item
20
+ if (kind === "index") return index
21
+ if (kind === "get") {
22
+ const object = evaluateCollectionExpression(parts[0], item, index)
23
+ return object == null && parts[2] ? undefined : object[parts[1]]
24
+ }
25
+ if (kind === "unary") {
26
+ const value = evaluateCollectionExpression(parts[1], item, index)
27
+ if (parts[0] === "!") return !value
28
+ if (parts[0] === "+") return +value
29
+ if (parts[0] === "-") return -value
30
+ if (parts[0] === "typeof") return typeof value
31
+ }
32
+ if (kind === "binary") {
33
+ const left = evaluateCollectionExpression(parts[1], item, index)
34
+ if (parts[0] === "&&") return left && evaluateCollectionExpression(parts[2], item, index)
35
+ if (parts[0] === "||") return left || evaluateCollectionExpression(parts[2], item, index)
36
+ if (parts[0] === "??") return left ?? evaluateCollectionExpression(parts[2], item, index)
37
+ const right = evaluateCollectionExpression(parts[2], item, index)
38
+ if (parts[0] === "===") return left === right
39
+ if (parts[0] === "!==") return left !== right
40
+ if (parts[0] === "==") return left == right
41
+ if (parts[0] === "!=") return left != right
42
+ if (parts[0] === "<") return left < right
43
+ if (parts[0] === "<=") return left <= right
44
+ if (parts[0] === ">") return left > right
45
+ if (parts[0] === ">=") return left >= right
46
+ if (parts[0] === "+") return left + right
47
+ if (parts[0] === "-") return left - right
48
+ if (parts[0] === "*") return left * right
49
+ if (parts[0] === "/") return left / right
50
+ if (parts[0] === "%") return left % right
51
+ }
52
+ if (kind === "conditional") return evaluateCollectionExpression(parts[0], item, index) ? evaluateCollectionExpression(parts[1], item, index) : evaluateCollectionExpression(parts[2], item, index)
53
+ if (kind === "array") return parts.map(value => evaluateCollectionExpression(value, item, index))
54
+ if (kind === "object") return Object.fromEntries(parts.map(([key, value]) => [key, evaluateCollectionExpression(value, item, index)]))
55
+ if (kind === "template") return parts[0].map((text, offset) => text + (offset < parts[1].length ? evaluateCollectionExpression(parts[1][offset], item, index) : "")).join("")
56
+ if (kind === "call") {
57
+ const receiver = evaluateCollectionExpression(parts[0], item, index)
58
+ return receiver[parts[1]](...parts.slice(2).map(value => evaluateCollectionExpression(value, item, index)))
59
+ }
60
+ if (kind === "global") return globalThis[parts[0]](...parts.slice(1).map(value => evaluateCollectionExpression(value, item, index)))
61
+ if (kind === "math") return Math[parts[0]](...parts.slice(1).map(value => evaluateCollectionExpression(value, item, index)))
62
+ throw new Error(`Unsupported rendered collection expression: ${String(kind)}`)
63
+ }
@@ -27,10 +27,11 @@ export function nativeBehavior(module: string, handler: string, states: Array<[s
27
27
  export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
28
28
  export function bindingValue(value: unknown): unknown
29
29
  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
30
- export function list(items: unknown, keyField: string, render: (item: unknown) => unknown, ownerField?: string): unknown
30
+ export function list(items: unknown, keyField: string | null, render: (item: unknown, index: number) => unknown, ownerField?: string, selector?: unknown[], indexed?: boolean): unknown
31
31
  export function listField(read: () => unknown, field: string): unknown
32
32
  export function listExpression(read: () => unknown, module: string, handler: string): unknown
33
33
  export function listItem(): unknown
34
+ export function listIndex(): unknown
34
35
  export function listConditional(kind: "and" | "ternary", read: () => unknown, truthy: () => unknown, falsy: () => unknown, module: string, handler: string): unknown
35
36
 
36
37
  export type PageMetadata = {
@@ -1,4 +1,5 @@
1
1
  import { serializeStyle } from "./style.js"
2
+ import { selectCollection } from "./collection-selector.js"
2
3
 
3
4
  const signalMarker = Symbol("kudzu.signal")
4
5
  const setterMarker = Symbol("kudzu.setter")
@@ -12,6 +13,7 @@ const listMarker = Symbol("kudzu.list")
12
13
  const listFieldMarker = Symbol("kudzu.listField")
13
14
  const listExpressionMarker = Symbol("kudzu.listExpression")
14
15
  const listItemMarker = Symbol("kudzu.listItem")
16
+ const listIndexMarker = Symbol("kudzu.listIndex")
15
17
  const listConditionalMarker = Symbol("kudzu.listConditional")
16
18
  const refMarker = Symbol("kudzu.ref")
17
19
  const contextMarker = Symbol("kudzu.context")
@@ -116,9 +118,10 @@ export function useEffect(callback, dependencies, module, handler, states, scope
116
118
  if (!effects) throw new Error(`${source} useEffect() inside keyed lists must belong to the direct row component`)
117
119
  const index = effects.length
118
120
  if (renderContext.listTemplate) {
119
- owner = nextRenderId("e")
120
- renderContext.listEffectOwners.push(owner)
121
- list = true
121
+ const existing = renderContext.listEffectOwners[index]
122
+ owner = existing ?? nextRenderId("e")
123
+ renderContext.listEffectOwners[index] = owner
124
+ list = !existing
122
125
  } else {
123
126
  owner = renderContext.listEffectOwners[index]
124
127
  if (!owner) throw new Error(`${source} Keyed row effects must have the same hook order for every item`)
@@ -146,7 +149,8 @@ function validEffectDependency(value) {
146
149
  export function useRef(initialValue) {
147
150
  if (!renderContext) throw new Error("useRef() can only run while rendering a Kudzu component")
148
151
  if (initialValue !== null) throw new Error("Kudzu DOM refs must initialize with null")
149
- return { [refMarker]: true, id: nextRenderId("r"), current: null }
152
+ const row = Boolean(renderContext.listRoot || renderContext.listRowRoot)
153
+ return { [refMarker]: true, id: row ? nextRowRenderId("r") : nextRenderId("r"), current: null, row }
150
154
  }
151
155
 
152
156
  export function createContext(defaultValue) {
@@ -214,7 +218,7 @@ export function stateConditional(kind, state, truthy, falsy) {
214
218
  return { [conditionalMarker]: true, kind, value: state.value, truthy, falsy, state: state.id }
215
219
  }
216
220
 
217
- export function list(items, keyField, render, ownerField) {
221
+ export function list(items, keyField, render, ownerField, selector = [], indexed = false) {
218
222
  if (!items?.[signalMarker] || !Array.isArray(items.value)) throw new Error("A keyed list must use local array state")
219
223
  let values = items.value
220
224
  if (ownerField) {
@@ -222,11 +226,12 @@ export function list(items, keyField, render, ownerField) {
222
226
  if (!owner) throw new Error("A nested keyed list must be rendered inside a keyed row")
223
227
  renderContext.listFields?.add(ownerField)
224
228
  values = renderContext.listTemplate ? [] : owner.item?.[ownerField]
225
- if (!Array.isArray(values)) throw new Error(`Nested keyed list property "${ownerField}" must remain an array`)
229
+ if (!Array.isArray(values) && values != null) throw new Error(`Nested keyed list property "${ownerField}" must remain an array`)
226
230
  }
231
+ values = selectCollection(values, selector)
227
232
  const keys = new Set()
228
- for (const item of values) {
229
- const key = item?.[keyField]
233
+ for (const [index, item] of values.entries()) {
234
+ const key = keyField === null ? index : item?.[keyField]
230
235
  if (!validListKey(key)) throw new Error(`Keyed list key "${keyField}" must be a string or finite number`)
231
236
  assertListItem(item)
232
237
  assertListValue(item, new Set())
@@ -234,7 +239,7 @@ export function list(items, keyField, render, ownerField) {
234
239
  if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
235
240
  keys.add(token)
236
241
  }
237
- return { [listMarker]: true, items, values, keyField, render, ownerField }
242
+ return { [listMarker]: true, items, values, keyField, render, ownerField, selector, indexed }
238
243
  }
239
244
 
240
245
  export function listField(read, field) {
@@ -252,6 +257,10 @@ export function listItem() {
252
257
  return { [listItemMarker]: true }
253
258
  }
254
259
 
260
+ export function listIndex() {
261
+ return { [listIndexMarker]: true }
262
+ }
263
+
255
264
  export function listConditional(kind, read, truthy, falsy, module, handler) {
256
265
  renderContext?.handlerModules.add(module)
257
266
  return { [listConditionalMarker]: true, kind, value: renderContext?.listTemplate ? undefined : read(), truthy, falsy, module, handler }
@@ -320,6 +329,7 @@ function bindingDescriptor(value) {
320
329
 
321
330
  function serializeCapture(name, value, seen) {
322
331
  if (value?.[listItemMarker]) return { type: "list-item" }
332
+ if (value?.[listIndexMarker]) return { type: "list-index" }
323
333
  if (value?.[refMarker]) return { type: "ref", id: value.id }
324
334
  if (value?.[signalMarker]) return { type: "state", id: value.id }
325
335
  if (typeof value === "function" && value[reducerDispatchMarker]) throw new Error(`Native capture "${name}" cannot contain a reducer dispatch`)
@@ -355,7 +365,7 @@ function serializeCapture(name, value, seen) {
355
365
  }
356
366
 
357
367
  export async function renderPage(component, metadata = {}, props = {}, layout) {
358
- renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
368
+ renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
359
369
 
360
370
  try {
361
371
  const page = { [routeScopeMarker]: true, component, props }
@@ -546,7 +556,9 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
546
556
  if (node?.[listMarker]) return renderList(node, namespace, selectValue)
547
557
  if (node?.[listFieldMarker]) {
548
558
  if (renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) renderContext.listFields?.add(node.field)
549
- const marker = renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch ? ` data-k-list-text="${escapeAttribute(node.field)}"` : ""
559
+ const marker = renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch
560
+ ? sharedInitialListMarker() ? " data-k-list-text" : ` data-k-list-text="${escapeAttribute(node.field)}"`
561
+ : ""
550
562
  return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-text-end></template>`
551
563
  }
552
564
  if (node?.[listExpressionMarker]) {
@@ -571,6 +583,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
571
583
  const owner = renderContext.listRoot ?? renderContext.listRowRoot
572
584
  if (owner) owner.conditions = true
573
585
  const previousBranch = renderContext.listConditionalBranch
586
+ if (owner && previousBranch) owner.nestedConditions = true
574
587
  renderContext.listConditionalBranch = true
575
588
  let truthy
576
589
  let falsy
@@ -585,8 +598,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
585
598
  ? ""
586
599
  : node.kind === "and" && !node.value ? escapeHtml(renderFalsy(node.value)) : node.value ? truthy : falsy
587
600
  const initial = renderContext.listTemplate ? "" : ` data-k-list-current="${escapeAttribute(key)}"`
588
- const shared = renderContext.listInitialMarkers && !renderContext.listTemplate && owner?.descriptor.ownerField
589
- const condition = shared ? " data-k-list-condition" : ` data-k-list-condition='${escapeJsonAttribute(descriptor)}'`
601
+ const shared = renderContext.listInitialMarkers && !renderContext.listTemplate
602
+ const condition = shared
603
+ ? ` data-k-list-condition${owner.descriptor.conditionHandlers ? ` data-k-list-condition-handler="${escapeAttribute(node.handler)}"` : ""}`
604
+ : ` data-k-list-condition='${escapeJsonAttribute(descriptor)}'`
590
605
  const branches = shared ? "" : `<template data-k-list-true>${truthy}</template><template data-k-list-false>${falsy}</template>`
591
606
  return `<template${condition}${initial}>${branches}</template>${current}<template data-k-list-condition-end></template>`
592
607
  }
@@ -647,7 +662,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
647
662
  if (rawName === "children" || rawName === "key") continue
648
663
  if (rawName === "ref") {
649
664
  if (!value?.[refMarker]) throw new Error("ref must be created by useRef(null)")
650
- if (renderContext.listDepth) throw new Error("Refs are not supported in keyed lists")
665
+ if (renderContext.listDepth && !value.row) throw new Error("Refs in keyed lists must be declared by the keyed row component")
651
666
  attributes += ` data-k-ref="${value.id}"`
652
667
  continue
653
668
  }
@@ -679,7 +694,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
679
694
  const native = template
680
695
  attributes += ` data-k-native-${event}='${escapeJsonAttribute(native)}'`
681
696
  renderContext.events.push({ event, native })
682
- if (renderContext.listDepth && Object.values(template.scope).some(entry => entry?.type === "list-item")) listEvents.push([event, template])
697
+ if (renderContext.listDepth && Object.values(template.scope).some(entry => entry?.type === "list-item" || entry?.type === "list-index")) listEvents.push([event, template])
683
698
  renderContext.hasNativeBehaviors = true
684
699
  } else {
685
700
  throw new Error(`${rawName} must reference a compilable event handler`)
@@ -727,12 +742,12 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
727
742
  }
728
743
 
729
744
  if (attributeBindings.length) attributes += ` data-k-bind-attrs='${escapeJsonAttribute(attributeBindings)}'`
730
- if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
745
+ if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && listAttributes.length) attributes += sharedInitialListMarker() ? " data-k-list-attrs" : ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
731
746
  if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
732
747
  if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
733
748
  if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && directListText) {
734
749
  renderContext.listFields?.add(directListText.field)
735
- attributes += ` data-k-list-text="${escapeAttribute(directListText.field)}"`
750
+ attributes += sharedInitialListMarker() ? " data-k-list-text" : ` data-k-list-text="${escapeAttribute(directListText.field)}"`
736
751
  }
737
752
 
738
753
  if (tag === "option" && selectValue !== noSelectValue && String(optionValue(props)) === (selectValue == null ? "" : String(selectValue))) attributes += " selected"
@@ -746,13 +761,23 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
746
761
  return `<${tag}${attributes}>${children}</${tag}>`
747
762
  }
748
763
 
764
+ function sharedInitialListMarker() {
765
+ const owner = renderContext.listRoot ?? renderContext.listRowRoot
766
+ return renderContext.listInitialMarkers && !renderContext.listTemplate && !renderContext.listConditionalBranch && owner?.descriptor.ownerField
767
+ }
768
+
749
769
  async function renderList(node, namespace, selectValue) {
750
770
  if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
751
771
  const ownerRoot = node.ownerField ? renderContext.listRoot ?? renderContext.listRowRoot : undefined
752
772
  const ownerTemplate = Boolean(node.ownerField && renderContext.listTemplate)
753
- const id = node.ownerField ? nextRowListId() : nextRenderId("l")
754
- const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map(item => item[node.keyField]), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.items[reducerStateMarker] ? { reducer: true } : {}) }
755
- if (ownerTemplate) Object.assign(ownerRoot.descriptor, { child: { field: node.ownerField, key: node.keyField }, mount: true, nested: true })
773
+ const rowList = node.ownerField ? nextRowList() : undefined
774
+ const id = rowList?.id ?? nextRenderId("l")
775
+ const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.selector.length ? { selector: node.selector } : {}), ...(node.indexed ? { indexed: true } : {}), ...(!node.selector.length && node.keyField !== null && node.items[reducerStateMarker] ? { reducer: true } : {}) }
776
+ if (ownerTemplate) {
777
+ ownerRoot.descriptor.children ??= []
778
+ ownerRoot.descriptor.children.push({ id, field: node.ownerField, key: node.keyField, ...(node.selector.length ? { selector: node.selector } : {}) })
779
+ Object.assign(ownerRoot.descriptor, { mount: true, nested: true })
780
+ }
756
781
  renderContext.listDepth++
757
782
  const previousListRoot = renderContext.listRoot
758
783
  const previousListRowRoot = renderContext.listRowRoot
@@ -761,23 +786,26 @@ async function renderList(node, namespace, selectValue) {
761
786
  const previousListFields = renderContext.listFields
762
787
  const previousListEffectOwners = renderContext.listEffectOwners
763
788
  const previousListRowStates = renderContext.listRowStates
789
+ const previousListRowRefs = renderContext.listRowRefs
764
790
  const previousListRowConditions = renderContext.listRowConditions
765
791
  const previousListRowLists = renderContext.listRowLists
766
792
  try {
767
793
  renderContext.listTemplate = true
768
- renderContext.listEffectOwners = []
769
- renderContext.listRowStates = []
770
- renderContext.listRowConditions = []
771
- renderContext.listRowLists = []
794
+ renderContext.listEffectOwners = rowList?.effectOwners ?? []
795
+ renderContext.listRowStates = rowList?.rowStates ?? []
796
+ renderContext.listRowRefs = rowList?.rowRefs ?? []
797
+ renderContext.listRowConditions = rowList?.rowConditions ?? []
798
+ renderContext.listRowLists = rowList?.rowLists ?? []
772
799
  renderContext.listFields = new Set([node.keyField])
773
- const templateRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0, l: 0 } }
800
+ const templateRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, path: ownerRoot?.path ?? [], rowIndexes: { s: 0, r: 0, c: 0, l: 0 } }
774
801
  renderContext.listRoot = templateRoot
775
802
  renderContext.listRowRoot = templateRoot
776
- const template = await renderNode(node.render({}), namespace, selectValue)
803
+ const template = await renderNode(node.render({}, 0), namespace, selectValue)
777
804
  if (template.includes("data-k-native-") || template.includes("data-k-effects=") || template.includes("data-k-list=")) descriptor.mount = true
778
805
  if (template.includes("data-k-list=")) descriptor.nested = true
779
806
  if (template.includes("data-k-effects=")) descriptor.effects = true
780
807
  if (templateRoot.conditions) descriptor.conditions = true
808
+ if (templateRoot.nestedConditions) descriptor.conditionHandlers = true
781
809
  if (template.includes("data-k-list-text-end")) descriptor.textRanges = true
782
810
  if (template.includes("data-k-list-attrs")) descriptor.attributes = true
783
811
  if (template.includes("data-k-list-events")) descriptor.events = true
@@ -788,17 +816,25 @@ async function renderList(node, namespace, selectValue) {
788
816
  if (renderContext.listRowConditions.length) descriptor.rowConditions = renderContext.listRowConditions.map(({ id }) => id)
789
817
  descriptor.mount = true
790
818
  }
791
- const seed = node.ownerField ? undefined : listSeed(node.values, renderContext.listFields)
819
+ if (renderContext.listRowRefs.length) {
820
+ descriptor.rowRefs = renderContext.listRowRefs.map(({ id }) => id)
821
+ descriptor.mount = true
822
+ }
823
+ const seed = node.ownerField || node.selector.length || node.keyField === null ? undefined : listSeed(node.values, renderContext.listFields)
792
824
  if (seed) descriptor.seed = seed
793
825
  let current = ""
794
826
  renderContext.listTemplate = false
795
827
  renderContext.listInitialMarkers = Boolean(descriptor.conditions)
796
- for (const item of node.values) {
797
- renderContext.listRoot = { id, state: node.items.id, descriptor, key: item[node.keyField], template: false, effects: [], item, rowIndexes: { s: 0, c: 0, l: 0 } }
828
+ for (const [index, item] of node.values.entries()) {
829
+ const key = node.keyField === null ? index : item[node.keyField]
830
+ renderContext.listRoot = { id, state: node.items.id, descriptor, key, template: false, effects: [], item, path: [...(ownerRoot?.path ?? []), `${id}=${typeof key}:${key}`], rowIndexes: { s: 0, r: 0, c: 0, l: 0 } }
798
831
  renderContext.listRowRoot = renderContext.listRoot
799
- current += await renderNode(node.render(item), namespace, selectValue)
832
+ current += await renderNode(node.render(item, index), namespace, selectValue)
833
+ }
834
+ if (!node.ownerField || ownerTemplate && !rowList.planned) {
835
+ renderContext.lists.push(descriptor)
836
+ if (rowList) rowList.planned = true
800
837
  }
801
- if (!node.ownerField || ownerTemplate) renderContext.lists.push(descriptor)
802
838
  renderContext.hasBehaviors = true
803
839
  renderContext.hasLists = true
804
840
  const prototype = node.ownerField && !ownerTemplate ? "" : template
@@ -811,23 +847,24 @@ async function renderList(node, namespace, selectValue) {
811
847
  renderContext.listFields = previousListFields
812
848
  renderContext.listEffectOwners = previousListEffectOwners
813
849
  renderContext.listRowStates = previousListRowStates
850
+ renderContext.listRowRefs = previousListRowRefs
814
851
  renderContext.listRowConditions = previousListRowConditions
815
852
  renderContext.listRowLists = previousListRowLists
816
853
  renderContext.listDepth--
817
854
  }
818
855
  }
819
856
 
820
- function nextRowListId() {
857
+ function nextRowList() {
821
858
  const root = renderContext.listRoot ?? renderContext.listRowRoot
822
859
  const index = root.rowIndexes.l++
823
860
  if (renderContext.listTemplate) {
824
- const id = nextRenderId("l")
825
- renderContext.listRowLists[index] = { id }
826
- return id
861
+ const entry = renderContext.listRowLists[index] ?? { id: nextRenderId("l"), rowLists: [], rowStates: [], rowRefs: [], rowConditions: [], effectOwners: [], planned: false }
862
+ renderContext.listRowLists[index] = entry
863
+ return entry
827
864
  }
828
865
  const entry = renderContext.listRowLists[index]
829
866
  if (!entry) throw new Error("Nested keyed lists must have the same order for every parent item")
830
- return entry.id
867
+ return entry
831
868
  }
832
869
 
833
870
  function nextRenderId(kind) {
@@ -839,19 +876,21 @@ function nextRenderId(kind) {
839
876
  function nextRowRenderId(kind, initialValue) {
840
877
  const root = renderContext.listRoot ?? renderContext.listRowRoot
841
878
  const index = root.rowIndexes[kind]++
842
- const entries = kind === "s" ? renderContext.listRowStates : renderContext.listRowConditions
879
+ const entries = kind === "s" ? renderContext.listRowStates : kind === "r" ? renderContext.listRowRefs : renderContext.listRowConditions
843
880
  if (renderContext.listTemplate) {
881
+ const entry = entries[index]
882
+ if (entry) return entry.id
844
883
  const id = `${nextRenderId(kind)}:$k`
845
884
  entries[index] = kind === "s" ? { id, initialValue } : { id }
846
885
  return id
847
886
  }
848
887
  const entry = entries[index]
849
- if (!entry) throw new Error(`Keyed row ${kind === "s" ? "state hooks" : "conditionals"} must have the same order for every item`)
850
- return rowRenderId(entry.id, root.key)
888
+ if (!entry) throw new Error(`Keyed row ${kind === "s" ? "state hooks" : kind === "r" ? "ref hooks" : "conditionals"} must have the same order for every item`)
889
+ return rowRenderId(entry.id, root.descriptor.ownerField ? root.path : [`${typeof root.key}:${root.key}`])
851
890
  }
852
891
 
853
- function rowRenderId(id, key) {
854
- return id.replace("$k", encodeURIComponent(`${typeof key}:${key}`))
892
+ function rowRenderId(id, path) {
893
+ return id.replace("$k", encodeURIComponent(path.join("/")))
855
894
  }
856
895
 
857
896
  function optionValue(props) {