@kudzujs/core 0.6.27 → 0.7.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.
@@ -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
+ }
@@ -3,6 +3,7 @@ export type Reducer<State, Action> = (state: State, action: Action) => State
3
3
  export type Dispatch<Action> = (action: Action) => void
4
4
  export type EffectCleanup = () => void | Promise<void>
5
5
  export type EffectDependency = string | number | boolean | null
6
+ export const Fragment: unique symbol
6
7
 
7
8
  export function useState<T>(initialValue: T): [T, StateSetter<T>]
8
9
  export function useReducer<State, Action>(reducer: Reducer<State, Action>, initialValue: State): [State, Dispatch<Action>]
@@ -22,15 +23,19 @@ export interface Context<T> {
22
23
  export function createContext<T>(defaultValue: T): Context<T>
23
24
  export function useContext<T>(context: Context<T>): T
24
25
 
26
+ declare const React: { Fragment: typeof Fragment }
27
+ export default React
28
+
25
29
  export function behavior(commands: Array<["add" | "set" | "log", unknown, unknown]>): unknown
26
30
  export function nativeBehavior(module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
27
31
  export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
28
32
  export function bindingValue(value: unknown): unknown
29
33
  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
34
+ export function list(items: unknown, keyField: string | null, render: (item: unknown, index: number) => unknown, ownerField?: string, selector?: unknown[], indexed?: boolean): unknown
31
35
  export function listField(read: () => unknown, field: string): unknown
32
36
  export function listExpression(read: () => unknown, module: string, handler: string): unknown
33
37
  export function listItem(): unknown
38
+ export function listIndex(): unknown
34
39
  export function listConditional(kind: "and" | "ternary", read: () => unknown, truthy: () => unknown, falsy: () => unknown, module: string, handler: string): unknown
35
40
 
36
41
  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,12 +13,14 @@ 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")
18
20
  const contextProviderMarker = Symbol("kudzu.contextProvider")
19
21
  const routeScopeMarker = Symbol("kudzu.routeScope")
20
22
  const noSelectValue = Symbol("kudzu.no-select-value")
23
+ export const Fragment = Symbol.for("kudzu.fragment")
21
24
  const svgAttributeAliases = {
22
25
  clipRule: "clip-rule",
23
26
  colorInterpolation: "color-interpolation",
@@ -116,9 +119,10 @@ export function useEffect(callback, dependencies, module, handler, states, scope
116
119
  if (!effects) throw new Error(`${source} useEffect() inside keyed lists must belong to the direct row component`)
117
120
  const index = effects.length
118
121
  if (renderContext.listTemplate) {
119
- owner = nextRenderId("e")
120
- renderContext.listEffectOwners.push(owner)
121
- list = true
122
+ const existing = renderContext.listEffectOwners[index]
123
+ owner = existing ?? nextRenderId("e")
124
+ renderContext.listEffectOwners[index] = owner
125
+ list = !existing
122
126
  } else {
123
127
  owner = renderContext.listEffectOwners[index]
124
128
  if (!owner) throw new Error(`${source} Keyed row effects must have the same hook order for every item`)
@@ -146,7 +150,8 @@ function validEffectDependency(value) {
146
150
  export function useRef(initialValue) {
147
151
  if (!renderContext) throw new Error("useRef() can only run while rendering a Kudzu component")
148
152
  if (initialValue !== null) throw new Error("Kudzu DOM refs must initialize with null")
149
- return { [refMarker]: true, id: nextRenderId("r"), current: null }
153
+ const row = Boolean(renderContext.listRoot || renderContext.listRowRoot)
154
+ return { [refMarker]: true, id: row ? nextRowRenderId("r") : nextRenderId("r"), current: null, row }
150
155
  }
151
156
 
152
157
  export function createContext(defaultValue) {
@@ -166,6 +171,8 @@ export function useContext(context) {
166
171
  return context.defaultValue
167
172
  }
168
173
 
174
+ export default { Fragment }
175
+
169
176
  export function behavior(commands) {
170
177
  return {
171
178
  [behaviorMarker]: true,
@@ -214,7 +221,7 @@ export function stateConditional(kind, state, truthy, falsy) {
214
221
  return { [conditionalMarker]: true, kind, value: state.value, truthy, falsy, state: state.id }
215
222
  }
216
223
 
217
- export function list(items, keyField, render, ownerField) {
224
+ export function list(items, keyField, render, ownerField, selector = [], indexed = false) {
218
225
  if (!items?.[signalMarker] || !Array.isArray(items.value)) throw new Error("A keyed list must use local array state")
219
226
  let values = items.value
220
227
  if (ownerField) {
@@ -222,11 +229,12 @@ export function list(items, keyField, render, ownerField) {
222
229
  if (!owner) throw new Error("A nested keyed list must be rendered inside a keyed row")
223
230
  renderContext.listFields?.add(ownerField)
224
231
  values = renderContext.listTemplate ? [] : owner.item?.[ownerField]
225
- if (!Array.isArray(values)) throw new Error(`Nested keyed list property "${ownerField}" must remain an array`)
232
+ if (!Array.isArray(values) && values != null) throw new Error(`Nested keyed list property "${ownerField}" must remain an array`)
226
233
  }
234
+ values = selectCollection(values, selector)
227
235
  const keys = new Set()
228
- for (const item of values) {
229
- const key = item?.[keyField]
236
+ for (const [index, item] of values.entries()) {
237
+ const key = keyField === null ? index : item?.[keyField]
230
238
  if (!validListKey(key)) throw new Error(`Keyed list key "${keyField}" must be a string or finite number`)
231
239
  assertListItem(item)
232
240
  assertListValue(item, new Set())
@@ -234,7 +242,7 @@ export function list(items, keyField, render, ownerField) {
234
242
  if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
235
243
  keys.add(token)
236
244
  }
237
- return { [listMarker]: true, items, values, keyField, render, ownerField }
245
+ return { [listMarker]: true, items, values, keyField, render, ownerField, selector, indexed }
238
246
  }
239
247
 
240
248
  export function listField(read, field) {
@@ -252,6 +260,10 @@ export function listItem() {
252
260
  return { [listItemMarker]: true }
253
261
  }
254
262
 
263
+ export function listIndex() {
264
+ return { [listIndexMarker]: true }
265
+ }
266
+
255
267
  export function listConditional(kind, read, truthy, falsy, module, handler) {
256
268
  renderContext?.handlerModules.add(module)
257
269
  return { [listConditionalMarker]: true, kind, value: renderContext?.listTemplate ? undefined : read(), truthy, falsy, module, handler }
@@ -320,6 +332,7 @@ function bindingDescriptor(value) {
320
332
 
321
333
  function serializeCapture(name, value, seen) {
322
334
  if (value?.[listItemMarker]) return { type: "list-item" }
335
+ if (value?.[listIndexMarker]) return { type: "list-index" }
323
336
  if (value?.[refMarker]) return { type: "ref", id: value.id }
324
337
  if (value?.[signalMarker]) return { type: "state", id: value.id }
325
338
  if (typeof value === "function" && value[reducerDispatchMarker]) throw new Error(`Native capture "${name}" cannot contain a reducer dispatch`)
@@ -355,7 +368,7 @@ function serializeCapture(name, value, seen) {
355
368
  }
356
369
 
357
370
  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 }
371
+ 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
372
 
360
373
  try {
361
374
  const page = { [routeScopeMarker]: true, component, props }
@@ -573,6 +586,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
573
586
  const owner = renderContext.listRoot ?? renderContext.listRowRoot
574
587
  if (owner) owner.conditions = true
575
588
  const previousBranch = renderContext.listConditionalBranch
589
+ if (owner && previousBranch) owner.nestedConditions = true
576
590
  renderContext.listConditionalBranch = true
577
591
  let truthy
578
592
  let falsy
@@ -587,8 +601,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
587
601
  ? ""
588
602
  : node.kind === "and" && !node.value ? escapeHtml(renderFalsy(node.value)) : node.value ? truthy : falsy
589
603
  const initial = renderContext.listTemplate ? "" : ` data-k-list-current="${escapeAttribute(key)}"`
590
- const shared = renderContext.listInitialMarkers && !renderContext.listTemplate && owner?.descriptor.ownerField
591
- const condition = shared ? " data-k-list-condition" : ` data-k-list-condition='${escapeJsonAttribute(descriptor)}'`
604
+ const shared = renderContext.listInitialMarkers && !renderContext.listTemplate
605
+ const condition = shared
606
+ ? ` data-k-list-condition${owner.descriptor.conditionHandlers ? ` data-k-list-condition-handler="${escapeAttribute(node.handler)}"` : ""}`
607
+ : ` data-k-list-condition='${escapeJsonAttribute(descriptor)}'`
592
608
  const branches = shared ? "" : `<template data-k-list-true>${truthy}</template><template data-k-list-false>${falsy}</template>`
593
609
  return `<template${condition}${initial}>${branches}</template>${current}<template data-k-list-condition-end></template>`
594
610
  }
@@ -649,7 +665,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
649
665
  if (rawName === "children" || rawName === "key") continue
650
666
  if (rawName === "ref") {
651
667
  if (!value?.[refMarker]) throw new Error("ref must be created by useRef(null)")
652
- if (renderContext.listDepth) throw new Error("Refs are not supported in keyed lists")
668
+ if (renderContext.listDepth && !value.row) throw new Error("Refs in keyed lists must be declared by the keyed row component")
653
669
  attributes += ` data-k-ref="${value.id}"`
654
670
  continue
655
671
  }
@@ -681,7 +697,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
681
697
  const native = template
682
698
  attributes += ` data-k-native-${event}='${escapeJsonAttribute(native)}'`
683
699
  renderContext.events.push({ event, native })
684
- if (renderContext.listDepth && Object.values(template.scope).some(entry => entry?.type === "list-item")) listEvents.push([event, template])
700
+ if (renderContext.listDepth && Object.values(template.scope).some(entry => entry?.type === "list-item" || entry?.type === "list-index")) listEvents.push([event, template])
685
701
  renderContext.hasNativeBehaviors = true
686
702
  } else {
687
703
  throw new Error(`${rawName} must reference a compilable event handler`)
@@ -757,9 +773,14 @@ async function renderList(node, namespace, selectValue) {
757
773
  if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
758
774
  const ownerRoot = node.ownerField ? renderContext.listRoot ?? renderContext.listRowRoot : undefined
759
775
  const ownerTemplate = Boolean(node.ownerField && renderContext.listTemplate)
760
- const id = node.ownerField ? nextRowListId() : nextRenderId("l")
761
- 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 } : {}) }
762
- if (ownerTemplate) Object.assign(ownerRoot.descriptor, { child: { field: node.ownerField, key: node.keyField }, mount: true, nested: true })
776
+ const rowList = node.ownerField ? nextRowList() : undefined
777
+ const id = rowList?.id ?? nextRenderId("l")
778
+ 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 } : {}) }
779
+ if (ownerTemplate) {
780
+ ownerRoot.descriptor.children ??= []
781
+ ownerRoot.descriptor.children.push({ id, field: node.ownerField, key: node.keyField, ...(node.selector.length ? { selector: node.selector } : {}) })
782
+ Object.assign(ownerRoot.descriptor, { mount: true, nested: true })
783
+ }
763
784
  renderContext.listDepth++
764
785
  const previousListRoot = renderContext.listRoot
765
786
  const previousListRowRoot = renderContext.listRowRoot
@@ -768,23 +789,26 @@ async function renderList(node, namespace, selectValue) {
768
789
  const previousListFields = renderContext.listFields
769
790
  const previousListEffectOwners = renderContext.listEffectOwners
770
791
  const previousListRowStates = renderContext.listRowStates
792
+ const previousListRowRefs = renderContext.listRowRefs
771
793
  const previousListRowConditions = renderContext.listRowConditions
772
794
  const previousListRowLists = renderContext.listRowLists
773
795
  try {
774
796
  renderContext.listTemplate = true
775
- renderContext.listEffectOwners = []
776
- renderContext.listRowStates = []
777
- renderContext.listRowConditions = []
778
- renderContext.listRowLists = []
797
+ renderContext.listEffectOwners = rowList?.effectOwners ?? []
798
+ renderContext.listRowStates = rowList?.rowStates ?? []
799
+ renderContext.listRowRefs = rowList?.rowRefs ?? []
800
+ renderContext.listRowConditions = rowList?.rowConditions ?? []
801
+ renderContext.listRowLists = rowList?.rowLists ?? []
779
802
  renderContext.listFields = new Set([node.keyField])
780
- const templateRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0, l: 0 } }
803
+ const templateRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, path: ownerRoot?.path ?? [], rowIndexes: { s: 0, r: 0, c: 0, l: 0 } }
781
804
  renderContext.listRoot = templateRoot
782
805
  renderContext.listRowRoot = templateRoot
783
- const template = await renderNode(node.render({}), namespace, selectValue)
806
+ const template = await renderNode(node.render({}, 0), namespace, selectValue)
784
807
  if (template.includes("data-k-native-") || template.includes("data-k-effects=") || template.includes("data-k-list=")) descriptor.mount = true
785
808
  if (template.includes("data-k-list=")) descriptor.nested = true
786
809
  if (template.includes("data-k-effects=")) descriptor.effects = true
787
810
  if (templateRoot.conditions) descriptor.conditions = true
811
+ if (templateRoot.nestedConditions) descriptor.conditionHandlers = true
788
812
  if (template.includes("data-k-list-text-end")) descriptor.textRanges = true
789
813
  if (template.includes("data-k-list-attrs")) descriptor.attributes = true
790
814
  if (template.includes("data-k-list-events")) descriptor.events = true
@@ -795,17 +819,25 @@ async function renderList(node, namespace, selectValue) {
795
819
  if (renderContext.listRowConditions.length) descriptor.rowConditions = renderContext.listRowConditions.map(({ id }) => id)
796
820
  descriptor.mount = true
797
821
  }
798
- const seed = node.ownerField ? undefined : listSeed(node.values, renderContext.listFields)
822
+ if (renderContext.listRowRefs.length) {
823
+ descriptor.rowRefs = renderContext.listRowRefs.map(({ id }) => id)
824
+ descriptor.mount = true
825
+ }
826
+ const seed = node.ownerField || node.selector.length || node.keyField === null ? undefined : listSeed(node.values, renderContext.listFields)
799
827
  if (seed) descriptor.seed = seed
800
828
  let current = ""
801
829
  renderContext.listTemplate = false
802
830
  renderContext.listInitialMarkers = Boolean(descriptor.conditions)
803
- for (const item of node.values) {
804
- renderContext.listRoot = { id, state: node.items.id, descriptor, key: item[node.keyField], template: false, effects: [], item, rowIndexes: { s: 0, c: 0, l: 0 } }
831
+ for (const [index, item] of node.values.entries()) {
832
+ const key = node.keyField === null ? index : item[node.keyField]
833
+ 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 } }
805
834
  renderContext.listRowRoot = renderContext.listRoot
806
- current += await renderNode(node.render(item), namespace, selectValue)
835
+ current += await renderNode(node.render(item, index), namespace, selectValue)
836
+ }
837
+ if (!node.ownerField || ownerTemplate && !rowList.planned) {
838
+ renderContext.lists.push(descriptor)
839
+ if (rowList) rowList.planned = true
807
840
  }
808
- if (!node.ownerField || ownerTemplate) renderContext.lists.push(descriptor)
809
841
  renderContext.hasBehaviors = true
810
842
  renderContext.hasLists = true
811
843
  const prototype = node.ownerField && !ownerTemplate ? "" : template
@@ -818,23 +850,24 @@ async function renderList(node, namespace, selectValue) {
818
850
  renderContext.listFields = previousListFields
819
851
  renderContext.listEffectOwners = previousListEffectOwners
820
852
  renderContext.listRowStates = previousListRowStates
853
+ renderContext.listRowRefs = previousListRowRefs
821
854
  renderContext.listRowConditions = previousListRowConditions
822
855
  renderContext.listRowLists = previousListRowLists
823
856
  renderContext.listDepth--
824
857
  }
825
858
  }
826
859
 
827
- function nextRowListId() {
860
+ function nextRowList() {
828
861
  const root = renderContext.listRoot ?? renderContext.listRowRoot
829
862
  const index = root.rowIndexes.l++
830
863
  if (renderContext.listTemplate) {
831
- const id = nextRenderId("l")
832
- renderContext.listRowLists[index] = { id }
833
- return id
864
+ const entry = renderContext.listRowLists[index] ?? { id: nextRenderId("l"), rowLists: [], rowStates: [], rowRefs: [], rowConditions: [], effectOwners: [], planned: false }
865
+ renderContext.listRowLists[index] = entry
866
+ return entry
834
867
  }
835
868
  const entry = renderContext.listRowLists[index]
836
869
  if (!entry) throw new Error("Nested keyed lists must have the same order for every parent item")
837
- return entry.id
870
+ return entry
838
871
  }
839
872
 
840
873
  function nextRenderId(kind) {
@@ -846,19 +879,21 @@ function nextRenderId(kind) {
846
879
  function nextRowRenderId(kind, initialValue) {
847
880
  const root = renderContext.listRoot ?? renderContext.listRowRoot
848
881
  const index = root.rowIndexes[kind]++
849
- const entries = kind === "s" ? renderContext.listRowStates : renderContext.listRowConditions
882
+ const entries = kind === "s" ? renderContext.listRowStates : kind === "r" ? renderContext.listRowRefs : renderContext.listRowConditions
850
883
  if (renderContext.listTemplate) {
884
+ const entry = entries[index]
885
+ if (entry) return entry.id
851
886
  const id = `${nextRenderId(kind)}:$k`
852
887
  entries[index] = kind === "s" ? { id, initialValue } : { id }
853
888
  return id
854
889
  }
855
890
  const entry = entries[index]
856
- if (!entry) throw new Error(`Keyed row ${kind === "s" ? "state hooks" : "conditionals"} must have the same order for every item`)
857
- return rowRenderId(entry.id, root.key)
891
+ if (!entry) throw new Error(`Keyed row ${kind === "s" ? "state hooks" : kind === "r" ? "ref hooks" : "conditionals"} must have the same order for every item`)
892
+ return rowRenderId(entry.id, root.descriptor.ownerField ? root.path : [`${typeof root.key}:${root.key}`])
858
893
  }
859
894
 
860
- function rowRenderId(id, key) {
861
- return id.replace("$k", encodeURIComponent(`${typeof key}:${key}`))
895
+ function rowRenderId(id, path) {
896
+ return id.replace("$k", encodeURIComponent(path.join("/")))
862
897
  }
863
898
 
864
899
  function optionValue(props) {