@kudzujs/core 0.6.16 → 0.6.18
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 +16 -2
- package/framework/README.md +1 -1
- package/framework/binding-runtime.js +21 -12
- package/framework/build.mjs +97 -8
- package/framework/core.mjs +58 -10
- package/framework/list-runtime.js +152 -2
- package/framework/runtime.js +5 -2
- package/framework/shared-runtime.js +37 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ function Controls({ dispatch }: { dispatch: Dispatch<TodoAction> }) {
|
|
|
190
190
|
return <Controls dispatch={dispatch} />
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
Kudzu specializes that call at build time; no function prop or child component survives in the browser. The direct child may also be a keyed row such as `todos.map(todo => <Item key={todo.id} todo={todo} dispatch={dispatch} />)`. Its inline or simple `const` event handler receives the latest keyed item,
|
|
193
|
+
Kudzu specializes that call at build time; no function prop or child component survives in the browser. Reducers follow React's pure reducer contract. The direct child may also be a keyed row such as `todos.map(todo => <Item key={todo.id} todo={todo} dispatch={dispatch} />)`. Its inline or simple `const` event handler receives the latest keyed item. That row may declare one top-level `useState` with a primitive literal initial value; the existing list key owns its state across item updates and reorder, and removal releases it so a later re-add starts from the initializer. Reactive attributes and conditional DOM use the existing binding capability. Relative TypeScript constants and helpers used inside the handler are renamed for call-site safety and bundled into the parent handler graph. Lazy state or reducer initializers, multiple or non-keyed specialized local states, package, namespace, local, async, and generator reducers, package imports or child imports used outside event handlers, further dispatch forwarding, reducer-dispatch keyed-row effects, and reducer dispatch through context remain unsupported.
|
|
194
194
|
|
|
195
195
|
That specialized component may pass one inline or simple `const` callback containing dispatch to one relative-imported synchronous child with an intrinsic root:
|
|
196
196
|
|
|
@@ -737,7 +737,21 @@ Each keyed row owns one effect depending on `item.name`. The measured actions re
|
|
|
737
737
|
|
|
738
738
|
This is a post-initialization runtime microbenchmark, not an architecture-equivalent loading comparison. Kudzu and Astro emit all 1,000 rows in HTML while React, Vue, and Svelte use empty CSR shells, so their JavaScript, output, and build columns are observations rather than framework-size or startup claims. Once every target has 1,000 rows and effects ready, targeted changed-root notification reduces Kudzu's selected update from 6.2 to 3.4 ms, versus Vue at 4.7 ms, Svelte at 5.2 ms, and React at 12.3 ms. It adds 126 B gzip to Kudzu's initial graph. List reconciliation remains O(n), which dominates unrelated-field updates; Vue measures 2.3 ms there versus Kudzu's 2.9 ms. Astro is the hand-written direct-DOM lower bound.
|
|
739
739
|
|
|
740
|
-
|
|
740
|
+
### 1,000-item Keyed Row State
|
|
741
|
+
|
|
742
|
+
Row 500 enters local edit state, the list reverses while preserving that row and its input DOM identity, then the row is removed and the same key is re-added with fresh non-editing state. Every target passed seven fresh-profile correctness runs; timings start at click and stop only after row order, unique IDs, labels, local state, and DOM identity match.
|
|
743
|
+
|
|
744
|
+
| Framework | Initial rows | Initial JS gzip | Total output | Build | Edit | Reverse | Remove | Re-add |
|
|
745
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
746
|
+
| Astro native | Yes | **373 B** | **83.7 KB** | 926 ms | **1.6 ms** | **5.9 ms** | **1.2 ms** | **1.4 ms** |
|
|
747
|
+
| Kudzu | Yes | 8.5 KB | 570.7 KB | **449 ms** | 2.7 ms | 10.1 ms | 3.0 ms | 3.1 ms |
|
|
748
|
+
| Vue CSR | No | 24.4 KB | 61.6 KB | 820 ms | 2.8 ms | 11.7 ms | 4.0 ms | 4.2 ms |
|
|
749
|
+
| Svelte CSR | No | 13.1 KB | 33.8 KB | 918 ms | 2.6 ms | 47.9 ms | 4.5 ms | 5.4 ms |
|
|
750
|
+
| React CSR | No | 59.4 KB | 189.5 KB | 1,074 ms | 5.9 ms | 26.4 ms | 9.0 ms | 6.5 ms |
|
|
751
|
+
|
|
752
|
+
Kudzu builds fastest and beats React, Vue, and Svelte on all four operations. Pure reducer identity fast paths skip unchanged-item validation for reorder, one removal, and append while preserving direct keyed DOM identity. Astro remains the hand-written native lower bound. Kudzu's 570.7 KB output includes complete initial HTML plus per-row direct-patch descriptors; React, Vue, and Svelte ship CSR shells, so deploy size and loading architecture are not equivalent comparisons.
|
|
753
|
+
|
|
754
|
+
The general benchmark snapshot was collected on July 22, 2026, the keyed-effect comparison on July 27, and keyed-row-state on July 28 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
|
|
741
755
|
|
|
742
756
|
## Development
|
|
743
757
|
|
package/framework/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Inline SVG rendering normalizes an explicit set of common React presentation ali
|
|
|
25
25
|
|
|
26
26
|
Same-file and relative-imported components receiving a direct local-state array prop are specialized to intrinsic JSX before keyed-list analysis, so their component function is not retained in the browser. Handler modules are emitted only when a rendered descriptor references them, preventing specialized imported components from adding dead browser assets. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal, unrelated fields do nothing, and key changes remain remove plus mount. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
27
27
|
|
|
28
|
-
The reduced `useReducer` form reuses ordinary state slots. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A keyed-row handler reads the latest item through the existing list scope. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing primitive literal defaults in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
28
|
+
The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A keyed-row handler reads the latest item through the existing list scope. It may declare one top-level primitive-literal `useState`; compiler-generated state IDs combine its hook slot and list key, preserving state across updates and reorder while row unmount deletes it. Existing event, binding, and conditional capabilities consume those IDs. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing primitive literal defaults in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
29
29
|
|
|
30
30
|
`kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
|
|
31
31
|
|
|
@@ -30,7 +30,8 @@ export function patchBinding(node, target, value) {
|
|
|
30
30
|
} else if (target === "class" && (value == null || value === false)) {
|
|
31
31
|
node.removeAttribute("class")
|
|
32
32
|
} else if (target === "class") {
|
|
33
|
-
node.
|
|
33
|
+
if (node.namespaceURI === "http://www.w3.org/1999/xhtml") node.className = String(value)
|
|
34
|
+
else node.setAttribute("class", String(value))
|
|
34
35
|
} else if (value == null || (value === false && !isStringBooleanAttribute(target))) {
|
|
35
36
|
node.removeAttribute(target)
|
|
36
37
|
} else {
|
|
@@ -77,7 +78,7 @@ function mountBindings(root) {
|
|
|
77
78
|
if (node.dataset.kBindAttrs) descriptors.push(...JSON.parse(node.dataset.kBindAttrs).map(({ target, ...descriptor }) => [target, descriptor]))
|
|
78
79
|
for (const [target, descriptor] of descriptors) {
|
|
79
80
|
if (descriptor.state) {
|
|
80
|
-
const binding = { node, target, read: () => browserState.get(descriptor.state) }
|
|
81
|
+
const binding = { node, target, read: Object.hasOwn(descriptor, "truthy") ? () => browserState.get(descriptor.state) ? descriptor.truthy : descriptor.falsy : () => browserState.get(descriptor.state) }
|
|
81
82
|
register(bindingTargets, descriptor.state, binding)
|
|
82
83
|
registrations.push([descriptor.state, binding])
|
|
83
84
|
patchBinding(node, target, binding.read())
|
|
@@ -124,8 +125,8 @@ function mountConditions(root) {
|
|
|
124
125
|
const truthy = start.content.querySelector("template[data-k-true]")
|
|
125
126
|
const falsy = start.content.querySelector("template[data-k-false]")
|
|
126
127
|
if (!end || !truthy || !falsy) continue
|
|
127
|
-
const condition = { start, end, truthy, falsy, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial) }
|
|
128
|
-
|
|
128
|
+
const condition = { start, end, truthy, falsy, kind: descriptor.kind, current: conditionKey(descriptor.kind, descriptor.initial), mount: descriptor.mount }
|
|
129
|
+
const mount = evaluator => {
|
|
129
130
|
if (!start.isConnected) return
|
|
130
131
|
condition.read = evaluator.read
|
|
131
132
|
const registrations = []
|
|
@@ -135,7 +136,9 @@ function mountConditions(root) {
|
|
|
135
136
|
}
|
|
136
137
|
conditionRegistrations.set(start, { condition, registrations })
|
|
137
138
|
updateCondition(condition)
|
|
138
|
-
}
|
|
139
|
+
}
|
|
140
|
+
if (descriptor.state) mount({ read: () => browserState.get(descriptor.state), stateIds: [descriptor.state] })
|
|
141
|
+
else loadEvaluator(descriptor).then(mount).catch(error => console.error(error))
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -143,16 +146,16 @@ function updateCondition(condition) {
|
|
|
143
146
|
const value = condition.read()
|
|
144
147
|
const next = conditionKey(condition.kind, value)
|
|
145
148
|
if (next === condition.current) return
|
|
146
|
-
removeConditionRange(condition.start, condition.end)
|
|
149
|
+
removeConditionRange(condition.start, condition.end, condition.mount)
|
|
147
150
|
const truthy = Boolean(value)
|
|
148
151
|
const falseText = condition.kind === "and" && !truthy ? renderFalsy(value) : ""
|
|
149
152
|
const fragment = falseText
|
|
150
153
|
? textFragment(condition.end.ownerDocument, falseText)
|
|
151
154
|
: (truthy ? condition.truthy : condition.falsy).content.cloneNode(true)
|
|
152
|
-
const nodes = [...fragment.childNodes]
|
|
155
|
+
const nodes = condition.mount ? [...fragment.childNodes] : undefined
|
|
153
156
|
condition.end.parentNode.insertBefore(fragment, condition.end)
|
|
154
157
|
condition.current = next
|
|
155
|
-
for (const node of nodes) mountDom(node)
|
|
158
|
+
if (condition.mount) for (const node of nodes) mountDom(node)
|
|
156
159
|
const select = condition.start.closest("select[data-k-bind-value]")
|
|
157
160
|
for (const binding of new Set((bindingRegistrations.get(select) ?? []).map(([, entry]) => entry))) {
|
|
158
161
|
patchBinding(binding.node, binding.target, binding.read())
|
|
@@ -183,13 +186,16 @@ function unmountConditions(root) {
|
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
function removeConditionRange(start, end) {
|
|
189
|
+
function removeConditionRange(start, end, mount) {
|
|
190
|
+
if (start.nextSibling === end) return
|
|
187
191
|
const range = start.ownerDocument.createRange()
|
|
188
192
|
range.setStartAfter(start)
|
|
189
193
|
range.setEndBefore(end)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
if (mount) {
|
|
195
|
+
const root = range.commonAncestorContainer
|
|
196
|
+
const nodes = matching(root, "*").filter(node => range.comparePoint(node, 0) === 0)
|
|
197
|
+
for (const node of nodes) if (!nodes.some(parent => parent !== node && parent.contains(node))) unmountDom(node)
|
|
198
|
+
}
|
|
193
199
|
range.deleteContents()
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -208,6 +214,9 @@ function textFragment(document, value) {
|
|
|
208
214
|
}
|
|
209
215
|
|
|
210
216
|
function findEnd(start, id) {
|
|
217
|
+
for (let node = start.nextSibling; node; node = node.nextSibling) {
|
|
218
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.matches("template[data-k-if-end]") && node.dataset.kIfEnd === id) return node
|
|
219
|
+
}
|
|
211
220
|
return [...start.ownerDocument.querySelectorAll("template[data-k-if-end]")]
|
|
212
221
|
.find(node => node.dataset.kIfEnd === id)
|
|
213
222
|
}
|
package/framework/build.mjs
CHANGED
|
@@ -189,6 +189,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
189
189
|
const hasListExpressionAttributes = plans.some(plan => plan.lists.some(list => list.expressionAttributes))
|
|
190
190
|
const hasListSeeds = plans.some(plan => plan.lists.some(list => list.seed))
|
|
191
191
|
const hasListEffects = plans.some(plan => plan.lists.some(list => list.effects))
|
|
192
|
+
const hasListRowStates = plans.some(plan => plan.lists.some(list => list.rowStates))
|
|
192
193
|
const hasItemDependencies = plans.some(plan => plan.effects.some(effect => effect.itemDependencies?.length))
|
|
193
194
|
const hasListAsyncParts = hasListExpressions || hasListExpressionAttributes || hasListConditions
|
|
194
195
|
const hasListMounts = hasListConditions || plans.some(plan => plan.lists.some(list => list.mount))
|
|
@@ -270,7 +271,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
270
271
|
__KUDZU_LIST_EFFECTS__: String(hasListEffects),
|
|
271
272
|
__KUDZU_LIST_ASYNC_PARTS__: String(hasListAsyncParts),
|
|
272
273
|
__KUDZU_LIST_MOUNTS__: String(hasListMounts),
|
|
273
|
-
__KUDZU_LIST_ITEM_HOOKS__: String(hasItemDependencies)
|
|
274
|
+
__KUDZU_LIST_ITEM_HOOKS__: String(hasItemDependencies),
|
|
275
|
+
__KUDZU_LIST_ROW_STATES__: String(hasListRowStates)
|
|
274
276
|
})
|
|
275
277
|
}
|
|
276
278
|
if (hasNativeHandlers) {
|
|
@@ -1629,6 +1631,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1629
1631
|
let usesList = false
|
|
1630
1632
|
let usesListEffects = false
|
|
1631
1633
|
let usesListItem = false
|
|
1634
|
+
let usesRowState = false
|
|
1632
1635
|
|
|
1633
1636
|
const collect = node => {
|
|
1634
1637
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
@@ -1736,10 +1739,26 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1736
1739
|
}
|
|
1737
1740
|
const componentSpecializations = new WeakMap()
|
|
1738
1741
|
const reducerComponentCalls = new WeakSet()
|
|
1742
|
+
const reducerRowStateCalls = []
|
|
1739
1743
|
const specializedDeclarations = new WeakSet()
|
|
1740
1744
|
const stateBackedComponentFunctions = new WeakSet()
|
|
1741
1745
|
const stateBackedComponentRoots = []
|
|
1742
1746
|
let specializedImportIndex = 0
|
|
1747
|
+
const registerReducerRowState = (call, specialization) => {
|
|
1748
|
+
if (!specialization.rowState) return
|
|
1749
|
+
let owner
|
|
1750
|
+
for (let current = call.parent; current; current = current.parent) {
|
|
1751
|
+
if (isFunctionLike(current) && reducersByFunction.has(current)) {
|
|
1752
|
+
owner = current
|
|
1753
|
+
break
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
1757
|
+
setters.set(specialization.rowState.setter, specialization.rowState.state)
|
|
1758
|
+
settersByFunction.set(owner, setters)
|
|
1759
|
+
reducerRowStateCalls.push(call)
|
|
1760
|
+
usesRowState = true
|
|
1761
|
+
}
|
|
1743
1762
|
const mergeSpecializedImports = (root, componentSource, call) => {
|
|
1744
1763
|
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
1745
1764
|
for (const name of runtimeImportNames(componentSource, false)) if (referenceIdentifiers(root, name).length) fail(call, "Imported specialized component handlers may only use relative TypeScript runtime imports")
|
|
@@ -1837,6 +1856,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1837
1856
|
if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
|
|
1838
1857
|
const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Reducer-dispatch")
|
|
1839
1858
|
if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
|
|
1859
|
+
registerReducerRowState(call, specialization)
|
|
1840
1860
|
specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
|
|
1841
1861
|
componentSpecializations.set(call, specialization)
|
|
1842
1862
|
reducerComponentCalls.add(call)
|
|
@@ -1860,6 +1880,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1860
1880
|
if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
|
|
1861
1881
|
const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
|
|
1862
1882
|
if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
|
|
1883
|
+
registerReducerRowState(call, specialization)
|
|
1863
1884
|
specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
|
|
1864
1885
|
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
|
|
1865
1886
|
synthesizeTree(specialization.root)
|
|
@@ -1896,6 +1917,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1896
1917
|
return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
|
|
1897
1918
|
}))
|
|
1898
1919
|
const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
|
|
1920
|
+
for (const call of reducerRowStateCalls) if (!keyedComponentCalls.has(call)) fail(call, "Reducer-dispatch component useState() is only supported in a direct keyed row")
|
|
1899
1921
|
for (const name of listComponentNames) {
|
|
1900
1922
|
let component = components.get(name)
|
|
1901
1923
|
const local = Boolean(component)
|
|
@@ -1935,6 +1957,15 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1935
1957
|
originalParts.callback.equalsGreaterThanToken,
|
|
1936
1958
|
root
|
|
1937
1959
|
)
|
|
1960
|
+
if (specialization?.stateDeclarations.length) callback = factory.updateArrowFunction(
|
|
1961
|
+
callback,
|
|
1962
|
+
callback.modifiers,
|
|
1963
|
+
callback.typeParameters,
|
|
1964
|
+
callback.parameters,
|
|
1965
|
+
callback.type,
|
|
1966
|
+
callback.equalsGreaterThanToken,
|
|
1967
|
+
factory.createBlock([...specialization.stateDeclarations, factory.createReturnStatement(root)], true)
|
|
1968
|
+
)
|
|
1938
1969
|
if (callback !== originalParts.callback) {
|
|
1939
1970
|
ts.setParentRecursive(callback, false)
|
|
1940
1971
|
callback.parent = originalParts.callback.parent
|
|
@@ -1945,7 +1976,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1945
1976
|
calculation.parent = callback
|
|
1946
1977
|
validateListExpression(calculation, parts.item, originalParts.root, fail)
|
|
1947
1978
|
}
|
|
1948
|
-
validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions)
|
|
1979
|
+
validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization?.rowState)
|
|
1949
1980
|
if (specialization?.effects.length) {
|
|
1950
1981
|
usesListEffects = true
|
|
1951
1982
|
const statements = specialization.effects.map(entry => {
|
|
@@ -2070,7 +2101,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2070
2101
|
])
|
|
2071
2102
|
}
|
|
2072
2103
|
|
|
2073
|
-
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && (node.initializer.expression.text === "useState" && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
|
|
2104
|
+
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && ((node.initializer.expression.text === "useState" || node.initializer.expression.text === "__kRowUseState") && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
|
|
2074
2105
|
const stateElement = node.name.elements[0]
|
|
2075
2106
|
if (!stateElement || !ts.isBindingElement(stateElement) || !ts.isIdentifier(stateElement.name)) return node
|
|
2076
2107
|
const initializer = factory.updateCallExpression(node.initializer, node.initializer.expression, node.initializer.typeArguments, [
|
|
@@ -2170,8 +2201,14 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2170
2201
|
|
|
2171
2202
|
const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
|
|
2172
2203
|
if (nativeHandlers.length) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
|
|
2173
|
-
if (usesBinding)
|
|
2174
|
-
|
|
2204
|
+
if (usesBinding) {
|
|
2205
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
|
|
2206
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
|
|
2207
|
+
}
|
|
2208
|
+
if (usesConditional) {
|
|
2209
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("conditional"), factory.createIdentifier("__kConditional")))
|
|
2210
|
+
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("stateConditional"), factory.createIdentifier("__kStateConditional")))
|
|
2211
|
+
}
|
|
2175
2212
|
if (usesList) {
|
|
2176
2213
|
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("list"), factory.createIdentifier("__kList")))
|
|
2177
2214
|
behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listExpression"), factory.createIdentifier("__kListExpression")))
|
|
@@ -2181,6 +2218,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2181
2218
|
}
|
|
2182
2219
|
if (usesListItem && !usesList) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("listItem"), factory.createIdentifier("__kListItem")))
|
|
2183
2220
|
if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
|
|
2221
|
+
if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
|
|
2184
2222
|
if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
|
|
2185
2223
|
const behaviorImport = factory.createImportDeclaration(
|
|
2186
2224
|
undefined,
|
|
@@ -2399,7 +2437,7 @@ function insideJsxEventHandler(node, root) {
|
|
|
2399
2437
|
return false
|
|
2400
2438
|
}
|
|
2401
2439
|
|
|
2402
|
-
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions) {
|
|
2440
|
+
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState) {
|
|
2403
2441
|
const fail = (node, message) => {
|
|
2404
2442
|
throw sourceNodeError(node, sourceFile, message)
|
|
2405
2443
|
}
|
|
@@ -2424,6 +2462,13 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2424
2462
|
const condition = conditionalParts(expression)
|
|
2425
2463
|
if (condition && containsJsx(expression)) {
|
|
2426
2464
|
if (conditionDepth) fail(node, "Nested item conditions are not supported in keyed lists")
|
|
2465
|
+
if (rowState && referencedStateNames(condition.condition, setters).has(rowState.state)) {
|
|
2466
|
+
conditionDepth++
|
|
2467
|
+
visit(condition.truthy)
|
|
2468
|
+
visit(condition.falsy)
|
|
2469
|
+
conditionDepth--
|
|
2470
|
+
return
|
|
2471
|
+
}
|
|
2427
2472
|
if (!referencesIdentifier(condition.condition, item)) fail(node, "Keyed list item conditions must read the item")
|
|
2428
2473
|
validateListExpression(condition.condition, item, node, fail)
|
|
2429
2474
|
listConditions.set(node.expression, { ...condition, item })
|
|
@@ -2489,6 +2534,8 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2489
2534
|
let returned
|
|
2490
2535
|
const calculations = []
|
|
2491
2536
|
const effectCalls = []
|
|
2537
|
+
const stateDeclarations = []
|
|
2538
|
+
let rowState
|
|
2492
2539
|
if (!ts.isBlock(component.body)) {
|
|
2493
2540
|
returned = component.body
|
|
2494
2541
|
} else {
|
|
@@ -2502,6 +2549,25 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2502
2549
|
}
|
|
2503
2550
|
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, `${label} component locals must be single const declarations`)
|
|
2504
2551
|
const declaration = statement.declarationList.declarations[0]
|
|
2552
|
+
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
|
|
2553
|
+
if (label !== "Reducer-dispatch") fail(declaration, `${label} components cannot declare local state`)
|
|
2554
|
+
if (rowState) throw sourceNodeError(declaration, component.getSourceFile(), "Reducer-dispatch keyed row components may declare exactly one top-level useState()")
|
|
2555
|
+
if (declaration.initializer.arguments.length !== 1 || !isPrimitiveDefaultLiteral(declaration.initializer.arguments[0])) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Reducer-dispatch keyed row useState() must use one primitive literal initial value; lazy initialization is not supported")
|
|
2556
|
+
if (!ts.isArrayBindingPattern(declaration.name) || declaration.name.elements.length !== 2 || declaration.name.elements.some(element => !element || !ts.isBindingElement(element) || !ts.isIdentifier(element.name) || element.initializer || element.dotDotDotToken)) throw sourceNodeError(declaration.name, component.getSourceFile(), "Reducer-dispatch keyed row useState() must use [state, setter] identifier destructuring")
|
|
2557
|
+
const suffix = Math.max(0, call.pos)
|
|
2558
|
+
const state = `__kRowState${suffix}`
|
|
2559
|
+
const setter = `__kRowSetter${suffix}`
|
|
2560
|
+
substitutions.set(declaration.name.elements[0].name.text, factory.createIdentifier(state))
|
|
2561
|
+
substitutions.set(declaration.name.elements[1].name.text, factory.createIdentifier(setter))
|
|
2562
|
+
const binding = factory.createArrayBindingPattern([
|
|
2563
|
+
factory.createBindingElement(undefined, undefined, factory.createIdentifier(state)),
|
|
2564
|
+
factory.createBindingElement(undefined, undefined, factory.createIdentifier(setter))
|
|
2565
|
+
])
|
|
2566
|
+
const initializer = factory.createCallExpression(factory.createIdentifier("__kRowUseState"), undefined, [cloneAst(declaration.initializer.arguments[0], factory, context)])
|
|
2567
|
+
stateDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
2568
|
+
rowState = { state, setter }
|
|
2569
|
+
continue
|
|
2570
|
+
}
|
|
2505
2571
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
|
|
2506
2572
|
const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
|
|
2507
2573
|
calculations.push({ name: declaration.name.text, expression: calculation })
|
|
@@ -2509,6 +2575,15 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2509
2575
|
}
|
|
2510
2576
|
returned = last.expression
|
|
2511
2577
|
}
|
|
2578
|
+
let unsupportedState
|
|
2579
|
+
const findUnsupportedState = node => {
|
|
2580
|
+
if (unsupportedState) return
|
|
2581
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useState") unsupportedState = node
|
|
2582
|
+
ts.forEachChild(node, findUnsupportedState)
|
|
2583
|
+
}
|
|
2584
|
+
findUnsupportedState(returned)
|
|
2585
|
+
for (const calculation of calculations) findUnsupportedState(calculation.expression)
|
|
2586
|
+
if (unsupportedState) throw sourceNodeError(unsupportedState, component.getSourceFile(), label === "Reducer-dispatch" ? "Reducer-dispatch keyed row useState() must be one top-level const declaration" : `${label} components cannot declare local state`)
|
|
2512
2587
|
let root = unwrapExpression(substituteClone(returned, substitutions, factory, context))
|
|
2513
2588
|
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
|
|
2514
2589
|
const tag = jsxTagName(root)
|
|
@@ -2524,7 +2599,9 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2524
2599
|
calculations: calculations
|
|
2525
2600
|
.filter(calculation => label !== "Reducer-dispatch" || !isFunctionLike(calculation.expression) || !isEventOnlyComponentLocal(returned, calculation.name))
|
|
2526
2601
|
.map(calculation => calculation.expression),
|
|
2527
|
-
effects
|
|
2602
|
+
effects,
|
|
2603
|
+
stateDeclarations,
|
|
2604
|
+
rowState
|
|
2528
2605
|
}
|
|
2529
2606
|
}
|
|
2530
2607
|
|
|
@@ -2801,15 +2878,27 @@ function isJsxLocalValue(expression, known) {
|
|
|
2801
2878
|
}
|
|
2802
2879
|
|
|
2803
2880
|
function compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl) {
|
|
2881
|
+
const parts = conditionalParts(expression)
|
|
2882
|
+
const state = parts && directStateIdentifier(parts.condition, setters)
|
|
2883
|
+
if (state && isPrimitiveDefaultLiteral(parts.truthy) && isPrimitiveDefaultLiteral(parts.falsy)) {
|
|
2884
|
+
return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
|
|
2885
|
+
}
|
|
2804
2886
|
return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl))
|
|
2805
2887
|
}
|
|
2806
2888
|
|
|
2807
2889
|
function compileConditional(kind, expression, truthy, falsy, setters, factory, context, reactiveBindings, handlerUrl) {
|
|
2808
|
-
const
|
|
2890
|
+
const state = directStateIdentifier(expression, setters)
|
|
2809
2891
|
const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
|
|
2892
|
+
if (state) return factory.createCallExpression(factory.createIdentifier("__kStateConditional"), undefined, [factory.createStringLiteral(kind), state, thunk(truthy), thunk(falsy)])
|
|
2893
|
+
const [initial, ...descriptor] = compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl)
|
|
2810
2894
|
return factory.createCallExpression(factory.createIdentifier("__kConditional"), undefined, [factory.createStringLiteral(kind), initial, thunk(truthy), thunk(falsy), ...descriptor])
|
|
2811
2895
|
}
|
|
2812
2896
|
|
|
2897
|
+
function directStateIdentifier(expression, setters) {
|
|
2898
|
+
const value = unwrapExpression(expression)
|
|
2899
|
+
return ts.isIdentifier(value) && new Set(setters.values()).has(value.text) ? value : undefined
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2813
2902
|
function compileReactiveExpression(expression, setters, factory, context, reactiveBindings, handlerUrl) {
|
|
2814
2903
|
const usedStates = referencedStateNames(expression, setters)
|
|
2815
2904
|
const captures = captureNames(expression, expression, setters)
|
package/framework/core.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { serializeStyle } from "./style.js"
|
|
|
3
3
|
const signalMarker = Symbol("kudzu.signal")
|
|
4
4
|
const setterMarker = Symbol("kudzu.setter")
|
|
5
5
|
const reducerDispatchMarker = Symbol("kudzu.reducerDispatch")
|
|
6
|
+
const reducerStateMarker = Symbol("kudzu.reducerState")
|
|
6
7
|
const behaviorMarker = Symbol("kudzu.behavior")
|
|
7
8
|
const nativeBehaviorMarker = Symbol("kudzu.nativeBehavior")
|
|
8
9
|
const bindingMarker = Symbol("kudzu.binding")
|
|
@@ -47,20 +48,21 @@ export function useState(initialValue, name) {
|
|
|
47
48
|
throw new Error("useState() can only run while rendering a Kudzu component")
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
const id = nextRenderId("s")
|
|
51
|
+
const id = renderContext.listRoot || renderContext.listRowRoot ? nextRowRenderId("s", initialValue) : nextRenderId("s")
|
|
51
52
|
const signal = createSignal(id, initialValue)
|
|
52
53
|
|
|
53
54
|
const setter = () => {
|
|
54
55
|
throw new Error("State setters are compiled into ordered browser behaviors")
|
|
55
56
|
}
|
|
56
57
|
Object.defineProperty(setter, setterMarker, { value: id })
|
|
57
|
-
renderContext.states[id] = { name: name ?? id, initialValue, ...(renderContext.scoped ? { lifetime: renderContext.renderScope } : {}) }
|
|
58
|
+
if (!renderContext.listTemplate) renderContext.states[id] = { name: name ?? id, initialValue, ...(renderContext.scoped ? { lifetime: renderContext.renderScope } : {}) }
|
|
58
59
|
return [signal, setter]
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export function useReducer(reducer, initialValue, name) {
|
|
62
63
|
if (typeof reducer !== "function") throw new Error("useReducer() requires a reducer function")
|
|
63
64
|
const [state] = useState(initialValue, name)
|
|
65
|
+
Object.defineProperty(state, reducerStateMarker, { value: true })
|
|
64
66
|
const dispatch = () => {
|
|
65
67
|
throw new Error("Reducer dispatches are compiled into browser handlers")
|
|
66
68
|
}
|
|
@@ -198,10 +200,20 @@ export function binding(value, module, handler, states, scope) {
|
|
|
198
200
|
return { [bindingMarker]: true, value, ...reactiveDescriptor(module, handler, states, scope) }
|
|
199
201
|
}
|
|
200
202
|
|
|
203
|
+
export function select(state, truthy, falsy) {
|
|
204
|
+
if (!state?.[signalMarker]) throw new Error("A reactive selection must target framework state")
|
|
205
|
+
return { [bindingMarker]: true, value: state.value ? truthy : falsy, state: state.id, truthy, falsy }
|
|
206
|
+
}
|
|
207
|
+
|
|
201
208
|
export function conditional(kind, value, truthy, falsy, module, handler, states, scope) {
|
|
202
209
|
return { [conditionalMarker]: true, kind, value, truthy, falsy, ...reactiveDescriptor(module, handler, states, scope) }
|
|
203
210
|
}
|
|
204
211
|
|
|
212
|
+
export function stateConditional(kind, state, truthy, falsy) {
|
|
213
|
+
if (!state?.[signalMarker]) throw new Error("A reactive conditional must target framework state")
|
|
214
|
+
return { [conditionalMarker]: true, kind, value: state.value, truthy, falsy, state: state.id }
|
|
215
|
+
}
|
|
216
|
+
|
|
205
217
|
export function list(items, keyField, render) {
|
|
206
218
|
if (!items?.[signalMarker] || !Array.isArray(items.value)) throw new Error("A keyed list must use local array state")
|
|
207
219
|
const keys = new Set()
|
|
@@ -294,6 +306,7 @@ export function bindingValue(value) {
|
|
|
294
306
|
}
|
|
295
307
|
|
|
296
308
|
function bindingDescriptor(value) {
|
|
309
|
+
if (value.state) return { state: value.state, ...(Object.hasOwn(value, "truthy") ? { truthy: value.truthy, falsy: value.falsy } : {}) }
|
|
297
310
|
return { module: value.module, handler: value.handler, states: value.states, scope: value.scope, scopeStates: value.scopeStates, scopeBindings: value.scopeBindings }
|
|
298
311
|
}
|
|
299
312
|
|
|
@@ -334,7 +347,7 @@ function serializeCapture(name, value, seen) {
|
|
|
334
347
|
}
|
|
335
348
|
|
|
336
349
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
337
|
-
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, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], 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 }
|
|
350
|
+
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: [], 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 }
|
|
338
351
|
|
|
339
352
|
try {
|
|
340
353
|
const page = { [routeScopeMarker]: true, component, props }
|
|
@@ -507,12 +520,13 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
507
520
|
if (!stateIds.size) return renderNode(node.value ? node.truthy() : node.falsy(), namespace, selectValue)
|
|
508
521
|
if (namespace) throw new Error(`Reactive conditional DOM is not supported inside ${namespace}`)
|
|
509
522
|
|
|
510
|
-
const id = nextRenderId("c")
|
|
523
|
+
const id = renderContext.listRoot || renderContext.listRowRoot ? nextRowRenderId("c") : nextRenderId("c")
|
|
511
524
|
renderContext.conditionDepth++
|
|
512
525
|
const truthy = await renderNode(node.truthy(), namespace, selectValue)
|
|
513
526
|
const falsy = await renderNode(node.falsy(), namespace, selectValue)
|
|
514
527
|
renderContext.conditionDepth--
|
|
515
|
-
const
|
|
528
|
+
const mount = truthy.includes("data-k-") || falsy.includes("data-k-") || truthy.includes("<!--k-text:") || falsy.includes("<!--k-text:")
|
|
529
|
+
const metadata = { id, kind: node.kind, initial: node.value, ...descriptor, ...(mount ? { mount: true } : {}) }
|
|
516
530
|
for (const stateId of stateIds) renderContext.conditionStates.add(stateId)
|
|
517
531
|
renderContext.conditions.push(metadata)
|
|
518
532
|
renderContext.hasBehaviors = true
|
|
@@ -642,8 +656,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
642
656
|
if (/^on[A-Z]/.test(rawName)) {
|
|
643
657
|
const event = rawName.slice(2).toLowerCase()
|
|
644
658
|
if (value?.[behaviorMarker]) {
|
|
645
|
-
const
|
|
646
|
-
attributes +=
|
|
659
|
+
const command = value.commands.length === 1 ? value.commands[0] : undefined
|
|
660
|
+
attributes += command?.[0] === "set" && command[2] === true
|
|
661
|
+
? ` data-k-set-true-${event}="${escapeAttribute(command[1])}"`
|
|
662
|
+
: ` data-k-on-${event}='${escapeJsonAttribute(value.commands)}'`
|
|
647
663
|
renderContext.events.push({ event, commands: value.commands })
|
|
648
664
|
} else if (value?.[nativeBehaviorMarker]) {
|
|
649
665
|
const template = { module: value.module, handler: value.handler, states: value.states, scope: value.scope }
|
|
@@ -720,15 +736,20 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
720
736
|
async function renderList(node, namespace, selectValue) {
|
|
721
737
|
if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
|
|
722
738
|
const id = nextRenderId("l")
|
|
723
|
-
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.items.value.map(item => item[node.keyField]) }
|
|
739
|
+
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.items.value.map(item => item[node.keyField]), ...(node.items[reducerStateMarker] ? { reducer: true } : {}) }
|
|
724
740
|
renderContext.listDepth++
|
|
725
741
|
const previousListFields = renderContext.listFields
|
|
726
742
|
const previousListEffectOwners = renderContext.listEffectOwners
|
|
743
|
+
const previousListRowStates = renderContext.listRowStates
|
|
744
|
+
const previousListRowConditions = renderContext.listRowConditions
|
|
727
745
|
try {
|
|
728
746
|
renderContext.listTemplate = true
|
|
729
747
|
renderContext.listEffectOwners = []
|
|
748
|
+
renderContext.listRowStates = []
|
|
749
|
+
renderContext.listRowConditions = []
|
|
730
750
|
renderContext.listFields = new Set([node.keyField])
|
|
731
|
-
renderContext.listRoot = { id, state: node.items.id, template: true, effects: [], item: {} }
|
|
751
|
+
renderContext.listRoot = { id, state: node.items.id, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0 } }
|
|
752
|
+
renderContext.listRowRoot = renderContext.listRoot
|
|
732
753
|
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
733
754
|
if (template.includes("data-k-native-") || template.includes("data-k-effects=")) descriptor.mount = true
|
|
734
755
|
if (template.includes("data-k-effects=")) descriptor.effects = true
|
|
@@ -738,13 +759,19 @@ async function renderList(node, namespace, selectValue) {
|
|
|
738
759
|
if (template.includes("data-k-list-events")) descriptor.events = true
|
|
739
760
|
if (template.includes("data-k-list-expression=")) descriptor.expressions = true
|
|
740
761
|
if (template.includes("data-k-list-expression-attrs")) descriptor.expressionAttributes = true
|
|
762
|
+
if (renderContext.listRowStates.length) {
|
|
763
|
+
descriptor.rowStates = renderContext.listRowStates
|
|
764
|
+
if (renderContext.listRowConditions.length) descriptor.rowConditions = renderContext.listRowConditions.map(({ id }) => id)
|
|
765
|
+
descriptor.mount = true
|
|
766
|
+
}
|
|
741
767
|
const seed = listSeed(node.items.value, renderContext.listFields)
|
|
742
768
|
if (seed) descriptor.seed = seed
|
|
743
769
|
let current = ""
|
|
744
770
|
renderContext.listTemplate = false
|
|
745
771
|
renderContext.listInitialMarkers = Boolean(descriptor.conditions)
|
|
746
772
|
for (const item of node.items.value) {
|
|
747
|
-
renderContext.listRoot = { id, state: node.items.id, key: item[node.keyField], template: false, effects: [], item }
|
|
773
|
+
renderContext.listRoot = { id, state: node.items.id, key: item[node.keyField], template: false, effects: [], item, rowIndexes: { s: 0, c: 0 } }
|
|
774
|
+
renderContext.listRowRoot = renderContext.listRoot
|
|
748
775
|
current += await renderNode(node.render(item), namespace, selectValue)
|
|
749
776
|
}
|
|
750
777
|
renderContext.lists.push(descriptor)
|
|
@@ -753,10 +780,13 @@ async function renderList(node, namespace, selectValue) {
|
|
|
753
780
|
return `<template data-k-list='${escapeJsonAttribute(descriptor)}'>${template}</template>${current}<template data-k-list-end="${id}"></template>`
|
|
754
781
|
} finally {
|
|
755
782
|
renderContext.listRoot = undefined
|
|
783
|
+
renderContext.listRowRoot = undefined
|
|
756
784
|
renderContext.listTemplate = false
|
|
757
785
|
renderContext.listInitialMarkers = false
|
|
758
786
|
renderContext.listFields = previousListFields
|
|
759
787
|
renderContext.listEffectOwners = previousListEffectOwners
|
|
788
|
+
renderContext.listRowStates = previousListRowStates
|
|
789
|
+
renderContext.listRowConditions = previousListRowConditions
|
|
760
790
|
renderContext.listDepth--
|
|
761
791
|
}
|
|
762
792
|
}
|
|
@@ -767,6 +797,24 @@ function nextRenderId(kind) {
|
|
|
767
797
|
return `${kind}${renderContext[counters[kind]]++}`
|
|
768
798
|
}
|
|
769
799
|
|
|
800
|
+
function nextRowRenderId(kind, initialValue) {
|
|
801
|
+
const root = renderContext.listRoot ?? renderContext.listRowRoot
|
|
802
|
+
const index = root.rowIndexes[kind]++
|
|
803
|
+
const entries = kind === "s" ? renderContext.listRowStates : renderContext.listRowConditions
|
|
804
|
+
if (renderContext.listTemplate) {
|
|
805
|
+
const id = `${nextRenderId(kind)}:$k`
|
|
806
|
+
entries[index] = kind === "s" ? { id, initialValue } : { id }
|
|
807
|
+
return id
|
|
808
|
+
}
|
|
809
|
+
const entry = entries[index]
|
|
810
|
+
if (!entry) throw new Error(`Keyed row ${kind === "s" ? "state hooks" : "conditionals"} must have the same order for every item`)
|
|
811
|
+
return rowRenderId(entry.id, root.key)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function rowRenderId(id, key) {
|
|
815
|
+
return id.replace("$k", encodeURIComponent(`${typeof key}:${key}`))
|
|
816
|
+
}
|
|
817
|
+
|
|
770
818
|
function optionValue(props) {
|
|
771
819
|
if (props.value != null) return bindingValue(props.value)
|
|
772
820
|
return Array.isArray(props.children) ? props.children.join("") : props.children ?? ""
|
|
@@ -31,6 +31,7 @@ function mountLists(root) {
|
|
|
31
31
|
const descriptor = JSON.parse(start.dataset.kList)
|
|
32
32
|
const end = findEnd(start, descriptor.id)
|
|
33
33
|
const roots = listRoots(start, end)
|
|
34
|
+
if (__KUDZU_LIST_ROW_STATES__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])
|
|
34
35
|
const templateRoot = start.content.firstElementChild
|
|
35
36
|
const parts = listItemPartPlan(templateRoot)
|
|
36
37
|
for (const root of roots) __KUDZU_LIST_CONDITIONS__ && descriptor.conditions ? listItemParts(root) : mapListItemParts(parts, root)
|
|
@@ -43,6 +44,7 @@ function mountLists(root) {
|
|
|
43
44
|
seedFields: __KUDZU_LIST_SEEDS__ && descriptor.seed && Object.keys(descriptor.seed),
|
|
44
45
|
roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
|
|
45
46
|
values: new Map(),
|
|
47
|
+
items: undefined,
|
|
46
48
|
container: roots[0]?.parentNode,
|
|
47
49
|
boundary: end
|
|
48
50
|
}
|
|
@@ -62,6 +64,7 @@ function unregisterList(start) {
|
|
|
62
64
|
const lists = listTargets.get(registration.state)
|
|
63
65
|
lists?.delete(registration.list)
|
|
64
66
|
if (!lists?.size) listTargets.delete(registration.state)
|
|
67
|
+
if (__KUDZU_LIST_ROW_STATES__ && registration.list.descriptor.rowStates) for (const token of registration.list.roots.keys()) deleteRowStates(registration.list.descriptor, token)
|
|
65
68
|
}
|
|
66
69
|
listRegistrations.delete(start)
|
|
67
70
|
mountedLists.delete(start)
|
|
@@ -70,6 +73,7 @@ function unregisterList(start) {
|
|
|
70
73
|
function updateList(list) {
|
|
71
74
|
const items = browserState.get(list.descriptor.state)
|
|
72
75
|
if (!Array.isArray(items)) throw new Error("Keyed list state must remain an array")
|
|
76
|
+
if (list.descriptor.reducer && list.items && updateReducerList(list, items)) return
|
|
73
77
|
const entries = []
|
|
74
78
|
const keys = new Set()
|
|
75
79
|
const seen = new Set()
|
|
@@ -82,7 +86,34 @@ function updateList(list) {
|
|
|
82
86
|
const token = keyToken(key)
|
|
83
87
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
84
88
|
keys.add(token)
|
|
85
|
-
entries.push({ item, key, token, value: seededValue
|
|
89
|
+
entries.push({ item, key, token, value: seededValue === undefined ? JSON.stringify(item) : seededValue })
|
|
90
|
+
}
|
|
91
|
+
if (list.values.size && entries.every(entry => !list.values.has(entry.token) || list.values.get(entry.token) === entry.value)) {
|
|
92
|
+
const currentTokens = [...list.roots.keys()]
|
|
93
|
+
const nextTokens = entries.map(entry => entry.token)
|
|
94
|
+
if (nextTokens.length === currentTokens.length && nextTokens.every((token, index) => token === currentTokens[currentTokens.length - index - 1])) {
|
|
95
|
+
const parent = list.container ?? list.start.parentNode
|
|
96
|
+
const reordered = parent.ownerDocument.createDocumentFragment()
|
|
97
|
+
reordered.append(...nextTokens.map(token => list.roots.get(token)))
|
|
98
|
+
parent.insertBefore(reordered, list.boundary)
|
|
99
|
+
list.roots = new Map(nextTokens.map(token => [token, list.roots.get(token)]))
|
|
100
|
+
list.container ??= parent
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
if (nextTokens.length === currentTokens.length - 1) {
|
|
104
|
+
const removed = currentTokens.find(token => !keys.has(token))
|
|
105
|
+
const removedIndex = currentTokens.indexOf(removed)
|
|
106
|
+
if (removed && nextTokens.every((token, index) => token === currentTokens[index >= removedIndex ? index + 1 : index])) {
|
|
107
|
+
removeListRoot(list, removed)
|
|
108
|
+
list.roots = new Map(nextTokens.map(token => [token, list.roots.get(token)]))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (nextTokens.length === currentTokens.length + 1 && currentTokens.every((token, index) => token === nextTokens[index])) {
|
|
113
|
+
const entry = entries.at(-1)
|
|
114
|
+
addListRoot(list, entry)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
86
117
|
}
|
|
87
118
|
const next = []
|
|
88
119
|
const values = new Map()
|
|
@@ -96,6 +127,7 @@ function updateList(list) {
|
|
|
96
127
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
97
128
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
98
129
|
node.removeAttribute("data-k-list-root")
|
|
130
|
+
if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
|
|
99
131
|
mapListItemParts(list.parts, node)
|
|
100
132
|
fillListItem(node, item)
|
|
101
133
|
additions.append(node)
|
|
@@ -113,6 +145,7 @@ function updateList(list) {
|
|
|
113
145
|
unmountDom(node)
|
|
114
146
|
node.remove()
|
|
115
147
|
} else node.remove()
|
|
148
|
+
if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, token)
|
|
116
149
|
}
|
|
117
150
|
if (added) {
|
|
118
151
|
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) {
|
|
@@ -143,6 +176,79 @@ function updateList(list) {
|
|
|
143
176
|
}
|
|
144
177
|
list.roots = new Map(next)
|
|
145
178
|
list.values = values
|
|
179
|
+
list.items = items
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function updateReducerList(list, items) {
|
|
183
|
+
const previous = list.items
|
|
184
|
+
if (items.length === previous.length && items.every((item, index) => item === previous[index])) {
|
|
185
|
+
list.items = items
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
if (items.length === previous.length && items.every((item, index) => item === previous[previous.length - index - 1])) {
|
|
189
|
+
const tokens = [...list.roots.keys()].reverse()
|
|
190
|
+
const parent = list.container ?? list.start.parentNode
|
|
191
|
+
const reordered = parent.ownerDocument.createDocumentFragment()
|
|
192
|
+
reordered.append(...tokens.map(token => list.roots.get(token)))
|
|
193
|
+
parent.insertBefore(reordered, list.boundary)
|
|
194
|
+
list.roots = new Map(tokens.map(token => [token, list.roots.get(token)]))
|
|
195
|
+
list.items = items
|
|
196
|
+
list.container ??= parent
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
if (items.length === previous.length - 1) {
|
|
200
|
+
let removed = 0
|
|
201
|
+
while (removed < items.length && items[removed] === previous[removed]) removed++
|
|
202
|
+
if (items.every((item, index) => item === previous[index >= removed ? index + 1 : index])) {
|
|
203
|
+
removeListRoot(list, keyToken(previous[removed]?.[list.descriptor.key]))
|
|
204
|
+
list.roots = new Map(items.map(item => {
|
|
205
|
+
const token = keyToken(item[list.descriptor.key])
|
|
206
|
+
return [token, list.roots.get(token)]
|
|
207
|
+
}))
|
|
208
|
+
list.items = items
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (items.length === previous.length + 1 && previous.every((item, index) => item === items[index])) {
|
|
213
|
+
const item = items.at(-1)
|
|
214
|
+
const key = item?.[list.descriptor.key]
|
|
215
|
+
if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
|
|
216
|
+
assertListItem(item)
|
|
217
|
+
const seen = new Set()
|
|
218
|
+
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.descriptor.seed) : undefined
|
|
219
|
+
if (seededValue === undefined) assertListValue(item, seen, true)
|
|
220
|
+
const token = keyToken(key)
|
|
221
|
+
if (list.roots.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
222
|
+
addListRoot(list, { item, key, token, value: seededValue === undefined ? JSON.stringify(item) : seededValue })
|
|
223
|
+
list.items = items
|
|
224
|
+
return true
|
|
225
|
+
}
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function addListRoot(list, { item, key, token, value }) {
|
|
230
|
+
let node = list.start.content.firstElementChild?.cloneNode(true)
|
|
231
|
+
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
232
|
+
if (!node) throw new Error("Keyed list template has no root element")
|
|
233
|
+
node.removeAttribute("data-k-list-root")
|
|
234
|
+
if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
|
|
235
|
+
mapListItemParts(list.parts, node)
|
|
236
|
+
fillListItem(node, item)
|
|
237
|
+
const parent = list.container ?? list.start.parentNode
|
|
238
|
+
parent.insertBefore(node, list.boundary)
|
|
239
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) mountDom(node)
|
|
240
|
+
list.roots.set(token, node)
|
|
241
|
+
list.values.set(token, value)
|
|
242
|
+
list.container ??= parent
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function removeListRoot(list, token) {
|
|
246
|
+
const node = list.roots.get(token)
|
|
247
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) unmountDom(node)
|
|
248
|
+
node.remove()
|
|
249
|
+
if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, token)
|
|
250
|
+
list.roots.delete(token)
|
|
251
|
+
list.values.delete(token)
|
|
146
252
|
}
|
|
147
253
|
|
|
148
254
|
function fillListItem(root, item) {
|
|
@@ -383,6 +489,40 @@ function keyToken(key) {
|
|
|
383
489
|
return `${typeof key}:${key}`
|
|
384
490
|
}
|
|
385
491
|
|
|
492
|
+
function initializeRowStates(descriptor, key, root) {
|
|
493
|
+
const token = keyToken(key)
|
|
494
|
+
const replacements = new Map()
|
|
495
|
+
for (const state of descriptor.rowStates) {
|
|
496
|
+
const id = rowStateId(state.id, token)
|
|
497
|
+
if (!browserState.has(id)) browserState.set(id, state.initialValue)
|
|
498
|
+
replacements.set(state.id, id)
|
|
499
|
+
}
|
|
500
|
+
for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, rowStateId(marker, token))
|
|
501
|
+
if (root) replaceRowIds(root, replacements)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function deleteRowStates(descriptor, token) {
|
|
505
|
+
for (const state of descriptor.rowStates) browserState.delete(rowStateId(state.id, token))
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function rowStateId(id, token) {
|
|
509
|
+
return id.replace("$k", encodeURIComponent(token))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function replaceRowIds(root, replacements) {
|
|
513
|
+
const replace = node => {
|
|
514
|
+
for (const attribute of [...node.attributes]) {
|
|
515
|
+
if (!attribute.name.startsWith("data-k-")) continue
|
|
516
|
+
let value = attribute.value
|
|
517
|
+
for (const [template, id] of replacements) if (value.includes(template)) value = value.replaceAll(template, id)
|
|
518
|
+
if (value !== attribute.value) attribute.value = value
|
|
519
|
+
}
|
|
520
|
+
for (const child of node.children) replace(child)
|
|
521
|
+
for (const child of node.content?.children ?? []) replace(child)
|
|
522
|
+
}
|
|
523
|
+
replace(root)
|
|
524
|
+
}
|
|
525
|
+
|
|
386
526
|
function validListKey(key) {
|
|
387
527
|
return typeof key === "string" || typeof key === "number" && Number.isFinite(key)
|
|
388
528
|
}
|
|
@@ -394,6 +534,16 @@ function assertListItem(item) {
|
|
|
394
534
|
|
|
395
535
|
function seededListValue(item, fields, seed) {
|
|
396
536
|
const keys = Reflect.ownKeys(item)
|
|
537
|
+
if (fields.length === 1) {
|
|
538
|
+
const field = fields[0]
|
|
539
|
+
if (keys.length !== 1 || keys[0] !== field) return undefined
|
|
540
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, field)
|
|
541
|
+
if (!descriptor.enumerable) throw new Error("Keyed list items must not contain non-enumerable properties")
|
|
542
|
+
if (!("value" in descriptor)) throw new Error("Keyed list items must not contain accessors")
|
|
543
|
+
const value = descriptor.value
|
|
544
|
+
const type = value === null ? "null" : typeof value
|
|
545
|
+
return type === seed[field] && !(type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) ? value : undefined
|
|
546
|
+
}
|
|
397
547
|
if (keys.length !== fields.length || keys.some(key => typeof key !== "string" || !fields.includes(key))) return undefined
|
|
398
548
|
const values = []
|
|
399
549
|
for (const field of fields) {
|
|
@@ -405,7 +555,7 @@ function seededListValue(item, fields, seed) {
|
|
|
405
555
|
if (type !== seed[field] || type === "number" && (!Number.isFinite(value) || Object.is(value, -0))) return undefined
|
|
406
556
|
values.push(value)
|
|
407
557
|
}
|
|
408
|
-
return JSON.stringify(values)
|
|
558
|
+
return values.length === 1 ? values[0] : JSON.stringify(values)
|
|
409
559
|
}
|
|
410
560
|
|
|
411
561
|
function assertListValue(value, seen, root = false) {
|
package/framework/runtime.js
CHANGED
|
@@ -24,7 +24,10 @@ if(typeof document!=="undefined"){
|
|
|
24
24
|
|
|
25
25
|
const eventNames = ["click", "input", "change"]
|
|
26
26
|
for(const eventName of eventNames)document.addEventListener(eventName,event=>{
|
|
27
|
-
const target=event.target.closest(`[data-k-on-${eventName}]`)
|
|
28
|
-
if(target)
|
|
27
|
+
const target=event.target.closest(`[data-k-set-true-${eventName}],[data-k-on-${eventName}]`)
|
|
28
|
+
if(!target)return
|
|
29
|
+
const direct=target.getAttribute(`data-k-set-true-${eventName}`)
|
|
30
|
+
if(direct){browserState.set(direct,true);commitDom(direct,true)}
|
|
31
|
+
else applyCommands(browserState,JSON.parse(target.getAttribute(`data-k-on-${eventName}`)),commitDom)
|
|
29
32
|
})
|
|
30
33
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export function applyCommands(state, commands, commit, log = console.log) {
|
|
2
|
+
if (commands.length === 1 && commands[0][0] !== "log") {
|
|
3
|
+
const [operation, id, operand] = commands[0]
|
|
4
|
+
const current = state.get(id)
|
|
5
|
+
const value = operation === "add" ? current + operand : operand
|
|
6
|
+
state.set(id, value)
|
|
7
|
+
commit(id, value)
|
|
8
|
+
return
|
|
9
|
+
}
|
|
2
10
|
const changed = new Set()
|
|
3
11
|
|
|
4
12
|
for (const [operation, id, operand] of commands) {
|
|
@@ -18,6 +26,8 @@ export const browserState = new Map()
|
|
|
18
26
|
const committers = []
|
|
19
27
|
const mountHooks = []
|
|
20
28
|
const unmountHooks = []
|
|
29
|
+
const textTargets = new Map()
|
|
30
|
+
const mountedText = new WeakSet()
|
|
21
31
|
|
|
22
32
|
/* list-item-hooks */
|
|
23
33
|
const listItemHooks = new Map()
|
|
@@ -50,15 +60,33 @@ export function registerUnmountHook(unmount) {
|
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export function commitDom(id, value) {
|
|
53
|
-
for (const node of
|
|
63
|
+
for (const node of textTargets.get(id) ?? []) {
|
|
64
|
+
if (node.isConnected) node.textContent = value
|
|
65
|
+
else textTargets.get(id).delete(node)
|
|
66
|
+
}
|
|
54
67
|
for (const commit of committers) commit(id)
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
export function mountText(root) {
|
|
58
71
|
for (const node of matching(root, "[data-k-text]")) {
|
|
72
|
+
if (mountedText.has(node)) continue
|
|
73
|
+
mountedText.add(node)
|
|
59
74
|
const id = node.dataset.kText
|
|
60
75
|
if (browserState.has(id)) node.textContent = browserState.get(id)
|
|
61
76
|
else browserState.set(id, JSON.parse(node.dataset.kValue))
|
|
77
|
+
const targets = textTargets.get(id) ?? new Set()
|
|
78
|
+
targets.add(node)
|
|
79
|
+
textTargets.set(id, targets)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function unmountText(root) {
|
|
84
|
+
for (const node of matching(root, "[data-k-text]")) {
|
|
85
|
+
const id = node.dataset.kText
|
|
86
|
+
const targets = textTargets.get(id)
|
|
87
|
+
targets?.delete(node)
|
|
88
|
+
if (!targets?.size) textTargets.delete(id)
|
|
89
|
+
mountedText.delete(node)
|
|
62
90
|
}
|
|
63
91
|
}
|
|
64
92
|
|
|
@@ -69,6 +97,7 @@ export function mountDom(root) {
|
|
|
69
97
|
|
|
70
98
|
export function unmountDom(root) {
|
|
71
99
|
for (const unmount of unmountHooks) unmount(root)
|
|
100
|
+
unmountText(root)
|
|
72
101
|
}
|
|
73
102
|
|
|
74
103
|
if (typeof document !== "undefined") {
|
|
@@ -79,8 +108,14 @@ if (typeof document !== "undefined") {
|
|
|
79
108
|
const eventNames = ["click", "input", "change"]
|
|
80
109
|
for (const eventName of eventNames) {
|
|
81
110
|
document.addEventListener(eventName, event => {
|
|
82
|
-
const target = event.target.closest(`[data-k-on-${eventName}]`)
|
|
111
|
+
const target = event.target.closest(`[data-k-set-true-${eventName}],[data-k-on-${eventName}]`)
|
|
83
112
|
if (!target) return
|
|
113
|
+
const direct = target.dataset[`kSetTrue${capitalize(eventName)}`]
|
|
114
|
+
if (direct) {
|
|
115
|
+
browserState.set(direct, true)
|
|
116
|
+
commitDom(direct, true)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
84
119
|
const commands = target.dataset[`kOn${capitalize(eventName)}`]
|
|
85
120
|
applyCommands(browserState, JSON.parse(commands), commitDom)
|
|
86
121
|
})
|