@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 +1 -1
- package/RELEASES.md +50 -0
- package/framework/build.mjs +77 -20
- package/framework/core.d.ts +1 -1
- package/framework/core.mjs +10 -4
- package/framework/list-runtime.js +36 -20
- package/package.json +1 -1
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.
|
|
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.
|
package/framework/build.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
4162
|
+
let collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
|
|
4159
4163
|
if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
|
|
4160
|
-
|
|
4164
|
+
let callback = value.arguments[0]
|
|
4161
4165
|
const parameters = collectionParameters(callback, "Nested keyed list map", fail)
|
|
4162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
6371
|
+
ts.factory.createBlock([ts.factory.createReturnStatement(transformed.transformed[0])], true)
|
|
6319
6372
|
)
|
|
6320
|
-
|
|
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) {
|
package/framework/core.d.ts
CHANGED
|
@@ -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
|
package/framework/core.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 (
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
})
|