@kudzujs/core 0.5.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GOAL_A.md +175 -0
- package/README.md +94 -10
- package/framework/README.md +9 -3
- package/framework/build.mjs +800 -43
- package/framework/core.d.ts +10 -4
- package/framework/core.mjs +117 -32
- package/framework/dependency-runtime.js +36 -0
- package/framework/effect-runtime.js +3 -1
- package/framework/list-runtime.js +13 -6
- package/framework/navigation-runtime.js +220 -0
- package/package.json +2 -1
package/framework/core.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export type StateSetter<T> = (value: T | ((previous: T) => T)) => void
|
|
2
2
|
export type EffectCleanup = () => void | Promise<void>
|
|
3
|
+
export type EffectDependency = string | number | boolean | null
|
|
3
4
|
|
|
4
5
|
export function useState<T>(initialValue: T): [T, StateSetter<T>]
|
|
5
|
-
export function useEffect(effect: () => void | EffectCleanup | Promise<void>, dependencies: readonly []): void
|
|
6
|
+
export function useEffect(effect: () => void | EffectCleanup | Promise<void>, dependencies: readonly EffectDependency[]): void
|
|
6
7
|
export function useParams<Params extends Record<string, string> = Record<string, string>>(): Readonly<Params>
|
|
7
8
|
|
|
8
9
|
export interface RefObject<T> {
|
|
@@ -49,11 +50,16 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
49
50
|
manifest?: string
|
|
50
51
|
styles?: boolean | string[]
|
|
51
52
|
base?: string
|
|
53
|
+
runtimeAsset?: string
|
|
52
54
|
effectAsset?: string
|
|
53
55
|
paramAsset?: string
|
|
54
56
|
runtimeParams?: string[]
|
|
57
|
+
navigationAsset?: string
|
|
58
|
+
applicationId?: string
|
|
59
|
+
layoutId?: string
|
|
55
60
|
},
|
|
56
|
-
props?: Props
|
|
61
|
+
props?: Props,
|
|
62
|
+
layout?: (props: { children: unknown }) => unknown | Promise<unknown>
|
|
57
63
|
): Promise<{
|
|
58
64
|
html: string
|
|
59
65
|
hasBehaviors: boolean
|
|
@@ -64,14 +70,14 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
64
70
|
hasListStyles: boolean
|
|
65
71
|
hasStateSeed: boolean
|
|
66
72
|
plan: {
|
|
67
|
-
states: Array<{ id: string; name: string; initialValue: unknown }>
|
|
73
|
+
states: Array<{ id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route" }>
|
|
68
74
|
params: Array<{ name: string; id: string }>
|
|
69
75
|
events: Array<{
|
|
70
76
|
event: string
|
|
71
77
|
commands?: Array<[string, string, unknown]>
|
|
72
78
|
native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
|
|
73
79
|
}>
|
|
74
|
-
effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; cleanup?: true }>
|
|
80
|
+
effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; lifetime?: "layout" | "route"; dependencies?: string[]; cleanup?: true; owner?: string; list?: true }>
|
|
75
81
|
bindings: Array<{
|
|
76
82
|
target: string
|
|
77
83
|
state?: string
|
package/framework/core.mjs
CHANGED
|
@@ -14,6 +14,7 @@ const listConditionalMarker = Symbol("kudzu.listConditional")
|
|
|
14
14
|
const refMarker = Symbol("kudzu.ref")
|
|
15
15
|
const contextMarker = Symbol("kudzu.context")
|
|
16
16
|
const contextProviderMarker = Symbol("kudzu.contextProvider")
|
|
17
|
+
const routeScopeMarker = Symbol("kudzu.routeScope")
|
|
17
18
|
const noSelectValue = Symbol("kudzu.no-select-value")
|
|
18
19
|
|
|
19
20
|
let renderContext
|
|
@@ -23,23 +24,24 @@ export function useState(initialValue, name) {
|
|
|
23
24
|
throw new Error("useState() can only run while rendering a Kudzu component")
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
const id =
|
|
27
|
+
const id = nextRenderId("s")
|
|
27
28
|
const signal = createSignal(id, initialValue)
|
|
28
29
|
|
|
29
30
|
const setter = () => {
|
|
30
31
|
throw new Error("State setters are compiled into ordered browser behaviors")
|
|
31
32
|
}
|
|
32
33
|
Object.defineProperty(setter, setterMarker, { value: id })
|
|
33
|
-
renderContext.states[id] = { name: name ?? id, initialValue }
|
|
34
|
+
renderContext.states[id] = { name: name ?? id, initialValue, ...(renderContext.scoped ? { lifetime: renderContext.renderScope } : {}) }
|
|
34
35
|
return [signal, setter]
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function useParams() {
|
|
39
|
+
if (renderContext?.renderScope === "layout") throw new Error("useParams() is only supported in route scope")
|
|
38
40
|
if (!renderContext?.runtimeParamNames?.length) throw new Error("useParams() requires export const runtimeParams = true on a bracket page")
|
|
39
41
|
if (!renderContext.params) {
|
|
40
42
|
const params = Object.create(null)
|
|
41
|
-
renderContext.paramEntries = renderContext.runtimeParamNames.map(
|
|
42
|
-
const id =
|
|
43
|
+
renderContext.paramEntries = renderContext.runtimeParamNames.map(name => {
|
|
44
|
+
const id = nextRenderId("p")
|
|
43
45
|
params[name] = createSignal(id, "")
|
|
44
46
|
return { name, id }
|
|
45
47
|
})
|
|
@@ -66,18 +68,45 @@ function createSignal(id, value) {
|
|
|
66
68
|
|
|
67
69
|
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup) {
|
|
68
70
|
if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
|
|
69
|
-
if (typeof callback !== "function" || !Array.isArray(dependencies) ||
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if (typeof callback !== "function" || !Array.isArray(dependencies) || !module || !handler) throw new Error("useEffect() must be compiled with a literal dependency array")
|
|
72
|
+
const dependencyIds = dependencies.map(dependency => {
|
|
73
|
+
if (!dependency?.[signalMarker] || !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
|
|
74
|
+
return dependency.id
|
|
75
|
+
})
|
|
76
|
+
let owner
|
|
77
|
+
let list = false
|
|
78
|
+
if (renderContext.listDepth) {
|
|
79
|
+
const effects = renderContext.listRoot?.effects
|
|
80
|
+
if (!effects) throw new Error(`${source} useEffect() inside keyed lists must belong to the direct row component`)
|
|
81
|
+
const index = effects.length
|
|
82
|
+
if (renderContext.listTemplate) {
|
|
83
|
+
owner = nextRenderId("e")
|
|
84
|
+
renderContext.listEffectOwners.push(owner)
|
|
85
|
+
list = true
|
|
86
|
+
} else {
|
|
87
|
+
owner = renderContext.listEffectOwners[index]
|
|
88
|
+
if (!owner) throw new Error(`${source} Keyed row effects must have the same hook order for every item`)
|
|
89
|
+
}
|
|
90
|
+
effects.push(owner)
|
|
91
|
+
} else if (renderContext.conditionDepth) {
|
|
92
|
+
const owners = renderContext.effectOwners.at(-1)
|
|
93
|
+
if (!owners) throw new Error(`${source} useEffect() inside conditional DOM must belong to a rendered function component`)
|
|
94
|
+
owner = nextRenderId("e")
|
|
95
|
+
owners.push(owner)
|
|
96
|
+
}
|
|
97
|
+
if (!renderContext.listDepth || list) renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
73
98
|
renderContext.hasBehaviors = true
|
|
74
99
|
renderContext.hasEffects = true
|
|
75
100
|
}
|
|
76
101
|
|
|
102
|
+
function validEffectDependency(value) {
|
|
103
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)
|
|
104
|
+
}
|
|
105
|
+
|
|
77
106
|
export function useRef(initialValue) {
|
|
78
107
|
if (!renderContext) throw new Error("useRef() can only run while rendering a Kudzu component")
|
|
79
108
|
if (initialValue !== null) throw new Error("Kudzu DOM refs must initialize with null")
|
|
80
|
-
return { [refMarker]: true, id:
|
|
109
|
+
return { [refMarker]: true, id: nextRenderId("r"), current: null }
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
export function createContext(defaultValue) {
|
|
@@ -261,18 +290,26 @@ function serializeCapture(name, value, seen) {
|
|
|
261
290
|
}
|
|
262
291
|
}
|
|
263
292
|
|
|
264
|
-
export async function renderPage(component, metadata = {}, props = {}) {
|
|
265
|
-
renderContext = { nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
293
|
+
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
294
|
+
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: [], runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
266
295
|
|
|
267
296
|
try {
|
|
268
|
-
const
|
|
297
|
+
const page = { [routeScopeMarker]: true, component, props }
|
|
298
|
+
const body = await renderNode(layout ? { type: layout, props: { children: page } } : { type: component, props })
|
|
269
299
|
renderContext.effects = renderContext.effects.map(effect => {
|
|
270
300
|
try {
|
|
301
|
+
if (metadata.navigationAsset && effect.owner) throw new Error("useEffect() inside conditional or keyed DOM is not supported in a configured navigation group yet; move the effect to the layout or route component body, or remove the route from navigation")
|
|
302
|
+
const descriptor = nativeDescriptor(effect.states.map(([name, read]) => [name, read()]), effect.scope.map(([name, read]) => [name, typeof read === "function" ? read() : read]))
|
|
271
303
|
return {
|
|
272
304
|
module: effect.module,
|
|
273
305
|
handler: effect.handler,
|
|
306
|
+
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
274
307
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
275
|
-
...
|
|
308
|
+
...(effect.owner ? { owner: effect.owner } : {}),
|
|
309
|
+
...(effect.list ? { list: true } : {}),
|
|
310
|
+
...(renderContext.scoped ? { lifetime: effect.renderScope } : {}),
|
|
311
|
+
states: descriptor.states,
|
|
312
|
+
scope: descriptor.scope
|
|
276
313
|
}
|
|
277
314
|
} catch (error) {
|
|
278
315
|
throw new Error(`${effect.source} ${error.message}`)
|
|
@@ -280,26 +317,30 @@ export async function renderPage(component, metadata = {}, props = {}) {
|
|
|
280
317
|
})
|
|
281
318
|
const title = escapeHtml(metadata.title ?? "Kudzu")
|
|
282
319
|
const head = renderMetadata(metadata)
|
|
320
|
+
const capability = metadata.navigationAsset ? " data-k-capability" : ""
|
|
283
321
|
const styles = metadata.styles === false
|
|
284
322
|
? ""
|
|
285
323
|
: (Array.isArray(metadata.styles) ? metadata.styles : [assetPath(metadata.base, "assets/style.css")]).map(href => `<link rel="stylesheet" href="${escapeAttribute(href)}">`).join("")
|
|
286
324
|
const runtime = renderContext.hasBehaviors
|
|
287
|
-
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu.js")}"></script>`
|
|
325
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.runtimeAsset ?? assetPath(metadata.base, "assets/kudzu.js"))}"></script>`
|
|
288
326
|
: ""
|
|
289
327
|
const nativeRuntime = renderContext.hasNativeBehaviors
|
|
290
|
-
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-native.js")}"></script>`
|
|
328
|
+
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-native.js")}"></script>`
|
|
291
329
|
: ""
|
|
292
330
|
const paramRuntime = renderContext.hasParams
|
|
293
|
-
? `<script type="module" src="${escapeAttribute(metadata.paramAsset)}"></script>`
|
|
331
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.paramAsset)}"></script>`
|
|
294
332
|
: ""
|
|
295
333
|
const bindingRuntime = renderContext.hasBindings
|
|
296
|
-
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-binding.js")}"></script>`
|
|
334
|
+
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-binding.js")}"></script>`
|
|
297
335
|
: ""
|
|
298
336
|
const listRuntime = renderContext.hasLists
|
|
299
|
-
? `<script type="module" src="${assetPath(metadata.base, "assets/kudzu-list.js")}"></script>`
|
|
337
|
+
? `<script type="module"${capability} src="${assetPath(metadata.base, "assets/kudzu-list.js")}"></script>`
|
|
300
338
|
: ""
|
|
301
339
|
const effectRuntime = renderContext.hasEffects
|
|
302
|
-
? `<script type="module" src="${escapeAttribute(metadata.effectAsset)}"></script>`
|
|
340
|
+
? `<script type="module"${capability} src="${escapeAttribute(metadata.effectAsset)}"></script>`
|
|
341
|
+
: ""
|
|
342
|
+
const navigationRuntime = metadata.navigationAsset
|
|
343
|
+
? `<script type="module" data-k-capability src="${escapeAttribute(metadata.navigationAsset)}"></script>`
|
|
303
344
|
: ""
|
|
304
345
|
const listStates = new Set(renderContext.lists.map(list => list.state))
|
|
305
346
|
const seededListStates = new Set(renderContext.lists.filter(list => list.seed && !renderContext.textStates.has(list.state) && !renderContext.conditionStates.has(list.state)).map(list => list.state))
|
|
@@ -317,7 +358,7 @@ export async function renderPage(component, metadata = {}, props = {}) {
|
|
|
317
358
|
: ""
|
|
318
359
|
|
|
319
360
|
return {
|
|
320
|
-
html: `<!doctype html><html lang="${escapeAttribute(metadata.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>${head}${styles}
|
|
361
|
+
html: `<!doctype html><html lang="${escapeAttribute(metadata.lang ?? "en")}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>${head}${styles}${runtime}${paramRuntime}${bindingRuntime}${listRuntime}${nativeRuntime}${effectRuntime}${navigationRuntime}</head><body${state}${textBindings}${metadata.applicationId ? ` data-k-application="${escapeAttribute(metadata.applicationId)}" data-k-layout="${escapeAttribute(metadata.layoutId)}"` : ""}>${body}</body></html>`,
|
|
321
362
|
hasBehaviors: renderContext.hasBehaviors,
|
|
322
363
|
hasEffects: renderContext.hasEffects,
|
|
323
364
|
hasParams: renderContext.hasParams,
|
|
@@ -342,16 +383,17 @@ export async function renderPage(component, metadata = {}, props = {}) {
|
|
|
342
383
|
|
|
343
384
|
function renderMetadata(metadata) {
|
|
344
385
|
const tags = []
|
|
386
|
+
const owned = metadata.navigationAsset ? " data-k-head" : ""
|
|
345
387
|
const meta = (name, content, property = false) => {
|
|
346
|
-
if (content) tags.push(`<meta ${property ? "property" : "name"}="${escapeAttribute(name)}" content="${escapeAttribute(content)}">`)
|
|
388
|
+
if (content) tags.push(`<meta${owned} ${property ? "property" : "name"}="${escapeAttribute(name)}" content="${escapeAttribute(content)}">`)
|
|
347
389
|
}
|
|
348
390
|
|
|
349
391
|
if (metadata.description) meta("description", metadata.description)
|
|
350
392
|
if (metadata.themeColor) meta("theme-color", metadata.themeColor)
|
|
351
|
-
if (metadata.url) tags.push(`<link rel="canonical" href="${escapeAttribute(metadata.url)}">`)
|
|
352
|
-
if (metadata.icon) tags.push(`<link rel="icon" href="${escapeAttribute(baseUrl(metadata.base, metadata.icon))}">`)
|
|
353
|
-
if (metadata.appleTouchIcon) tags.push(`<link rel="apple-touch-icon" href="${escapeAttribute(baseUrl(metadata.base, metadata.appleTouchIcon))}">`)
|
|
354
|
-
if (metadata.manifest) tags.push(`<link rel="manifest" href="${escapeAttribute(baseUrl(metadata.base, metadata.manifest))}">`)
|
|
393
|
+
if (metadata.url) tags.push(`<link${owned} rel="canonical" href="${escapeAttribute(metadata.url)}">`)
|
|
394
|
+
if (metadata.icon) tags.push(`<link${owned} rel="icon" href="${escapeAttribute(baseUrl(metadata.base, metadata.icon))}">`)
|
|
395
|
+
if (metadata.appleTouchIcon) tags.push(`<link${owned} rel="apple-touch-icon" href="${escapeAttribute(baseUrl(metadata.base, metadata.appleTouchIcon))}">`)
|
|
396
|
+
if (metadata.manifest) tags.push(`<link${owned} rel="manifest" href="${escapeAttribute(baseUrl(metadata.base, metadata.manifest))}">`)
|
|
355
397
|
|
|
356
398
|
meta("og:title", metadata.title, true)
|
|
357
399
|
meta("og:description", metadata.description, true)
|
|
@@ -397,6 +439,16 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
397
439
|
return escapeHtml(node)
|
|
398
440
|
}
|
|
399
441
|
if (node instanceof Promise) return renderNode(await node, namespace, selectValue)
|
|
442
|
+
if (node?.[routeScopeMarker]) {
|
|
443
|
+
const previousScope = renderContext.renderScope
|
|
444
|
+
renderContext.renderScope = "route"
|
|
445
|
+
try {
|
|
446
|
+
const html = await renderNode({ type: node.component, props: node.props }, namespace, selectValue)
|
|
447
|
+
return `<template data-k-route-start></template>${html}<template data-k-route-end></template>`
|
|
448
|
+
} finally {
|
|
449
|
+
renderContext.renderScope = previousScope
|
|
450
|
+
}
|
|
451
|
+
}
|
|
400
452
|
if (node?.[contextProviderMarker]) {
|
|
401
453
|
renderContext.contexts.push([node.context, node.value])
|
|
402
454
|
try {
|
|
@@ -411,7 +463,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
411
463
|
if (!stateIds.size) return renderNode(node.value ? node.truthy() : node.falsy(), namespace, selectValue)
|
|
412
464
|
if (namespace) throw new Error(`Reactive conditional DOM is not supported inside ${namespace}`)
|
|
413
465
|
|
|
414
|
-
const id =
|
|
466
|
+
const id = nextRenderId("c")
|
|
415
467
|
renderContext.conditionDepth++
|
|
416
468
|
const truthy = await renderNode(node.truthy(), namespace, selectValue)
|
|
417
469
|
const falsy = await renderNode(node.falsy(), namespace, selectValue)
|
|
@@ -472,10 +524,29 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
472
524
|
}
|
|
473
525
|
|
|
474
526
|
if (node.type === Symbol.for("kudzu.fragment")) return renderNode(node.props.children, namespace, selectValue)
|
|
475
|
-
if (typeof node.type === "function")
|
|
527
|
+
if (typeof node.type === "function") {
|
|
528
|
+
if (!renderContext.conditionDepth) return renderNode(await node.type(node.props), namespace, selectValue)
|
|
529
|
+
const owners = []
|
|
530
|
+
renderContext.effectOwners.push(owners)
|
|
531
|
+
let result
|
|
532
|
+
try {
|
|
533
|
+
result = await node.type(node.props)
|
|
534
|
+
} finally {
|
|
535
|
+
renderContext.effectOwners.pop()
|
|
536
|
+
}
|
|
537
|
+
const html = await renderNode(result, namespace, selectValue)
|
|
538
|
+
return owners.map(owner => `<template data-k-effect="${owner}"></template>`).join("") + html
|
|
539
|
+
}
|
|
476
540
|
|
|
477
541
|
const tag = node.type
|
|
478
542
|
const props = node.props ?? {}
|
|
543
|
+
if (typeof tag === "string" && tag.toLowerCase() === "link") {
|
|
544
|
+
const rel = Object.entries(props).find(([name]) => name.toLowerCase() === "rel")?.[1]
|
|
545
|
+
const value = rel?.[signalMarker] || rel?.[bindingMarker] ? rel.value : rel
|
|
546
|
+
if (typeof value === "string" && value.toLowerCase().split(/\s+/).includes("stylesheet")) {
|
|
547
|
+
throw new Error("Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
|
|
548
|
+
}
|
|
549
|
+
}
|
|
479
550
|
const directListText = props.children?.[listFieldMarker] ? props.children : undefined
|
|
480
551
|
const childSelectValue = tag === "select"
|
|
481
552
|
? Object.hasOwn(props, "value") ? bindingValue(props.value) : noSelectValue
|
|
@@ -494,6 +565,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
494
565
|
const root = renderContext.listRoot
|
|
495
566
|
renderContext.listRoot = undefined
|
|
496
567
|
if (root.template) attributes += ` data-k-list-root="${root.id}"`
|
|
568
|
+
if (root.effects.length) {
|
|
569
|
+
attributes += ` data-k-effects='${escapeJsonAttribute(root.effects)}'`
|
|
570
|
+
if (!root.template) attributes += ` data-k-effect-item='${escapeJsonAttribute(root.item)}'`
|
|
571
|
+
}
|
|
497
572
|
}
|
|
498
573
|
|
|
499
574
|
for (const [rawName, value] of Object.entries(props)) {
|
|
@@ -506,7 +581,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
506
581
|
}
|
|
507
582
|
if (rawName === "selected" && selectValue !== noSelectValue) continue
|
|
508
583
|
if (/^on/i.test(rawName) && !/^on[A-Z]/.test(rawName)) throw new Error(`${rawName} must use a camelCase event handler`)
|
|
509
|
-
if (rawName.toLowerCase().startsWith("data-k-")) throw new Error(`${rawName} uses Kudzu's reserved data-k-* prefix`)
|
|
584
|
+
if (rawName.toLowerCase().startsWith("data-k-") && rawName.toLowerCase() !== "data-k-native") throw new Error(`${rawName} uses Kudzu's reserved data-k-* prefix`)
|
|
510
585
|
if (["ref", "dangerouslysetinnerhtml"].includes(rawName.toLowerCase()) && (value?.[signalMarker] || value?.[bindingMarker])) {
|
|
511
586
|
throw new Error(`Reactive ${rawName} is not supported`)
|
|
512
587
|
}
|
|
@@ -599,16 +674,19 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
599
674
|
|
|
600
675
|
async function renderList(node, namespace, selectValue) {
|
|
601
676
|
if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
|
|
602
|
-
const id =
|
|
677
|
+
const id = nextRenderId("l")
|
|
603
678
|
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.items.value.map(item => item[node.keyField]) }
|
|
604
679
|
renderContext.listDepth++
|
|
605
680
|
const previousListFields = renderContext.listFields
|
|
681
|
+
const previousListEffectOwners = renderContext.listEffectOwners
|
|
606
682
|
try {
|
|
607
683
|
renderContext.listTemplate = true
|
|
684
|
+
renderContext.listEffectOwners = []
|
|
608
685
|
renderContext.listFields = new Set([node.keyField])
|
|
609
|
-
renderContext.listRoot = { id, template: true }
|
|
686
|
+
renderContext.listRoot = { id, template: true, effects: [], item: {} }
|
|
610
687
|
const template = await renderNode(node.render({}), namespace, selectValue)
|
|
611
|
-
if (template.includes("data-k-native-")) descriptor.mount = true
|
|
688
|
+
if (template.includes("data-k-native-") || template.includes("data-k-effects=")) descriptor.mount = true
|
|
689
|
+
if (template.includes("data-k-effects=")) descriptor.effects = true
|
|
612
690
|
if (template.includes("data-k-list-condition")) descriptor.conditions = true
|
|
613
691
|
if (template.includes("data-k-list-text-end")) descriptor.textRanges = true
|
|
614
692
|
if (template.includes("data-k-list-attrs")) descriptor.attributes = true
|
|
@@ -621,7 +699,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
621
699
|
renderContext.listTemplate = false
|
|
622
700
|
renderContext.listInitialMarkers = Boolean(descriptor.conditions)
|
|
623
701
|
for (const item of node.items.value) {
|
|
624
|
-
renderContext.listRoot = { id, key: item[node.keyField], template: false }
|
|
702
|
+
renderContext.listRoot = { id, key: item[node.keyField], template: false, effects: [], item }
|
|
625
703
|
current += await renderNode(node.render(item), namespace, selectValue)
|
|
626
704
|
}
|
|
627
705
|
renderContext.lists.push(descriptor)
|
|
@@ -633,10 +711,17 @@ async function renderList(node, namespace, selectValue) {
|
|
|
633
711
|
renderContext.listTemplate = false
|
|
634
712
|
renderContext.listInitialMarkers = false
|
|
635
713
|
renderContext.listFields = previousListFields
|
|
714
|
+
renderContext.listEffectOwners = previousListEffectOwners
|
|
636
715
|
renderContext.listDepth--
|
|
637
716
|
}
|
|
638
717
|
}
|
|
639
718
|
|
|
719
|
+
function nextRenderId(kind) {
|
|
720
|
+
if (renderContext.scoped) return `${renderContext.renderScope === "layout" ? "l" : "r"}${kind}${renderContext.counters[renderContext.renderScope][kind]++}`
|
|
721
|
+
const counters = { s: "nextState", r: "nextRef", c: "nextCondition", l: "nextList", e: "nextEffect", p: "nextParam" }
|
|
722
|
+
return `${kind}${renderContext[counters[kind]]++}`
|
|
723
|
+
}
|
|
724
|
+
|
|
640
725
|
function optionValue(props) {
|
|
641
726
|
if (props.value != null) return bindingValue(props.value)
|
|
642
727
|
return Array.isArray(props.children) ? props.children.join("") : props.children ?? ""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function applyCommands(state, commands, commit, log = console.log) {
|
|
2
|
+
const changed = new Set()
|
|
3
|
+
for (const [operation, id, operand] of commands) {
|
|
4
|
+
const current = state.get(id)
|
|
5
|
+
if (operation === "log") log(operand, current)
|
|
6
|
+
else {
|
|
7
|
+
state.set(id, operation === "add" ? current + operand : operand)
|
|
8
|
+
changed.add(id)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const id of changed) commit(id, state.get(id))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const browserState = new Map()
|
|
15
|
+
const committers = []
|
|
16
|
+
|
|
17
|
+
export function registerCommitter(commit) {
|
|
18
|
+
committers.push(commit)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function commitDom(id, value) {
|
|
22
|
+
for (const node of document.querySelectorAll(`[data-k-text="${id}"]`)) node.textContent = value
|
|
23
|
+
for (const commit of committers) commit(id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof document !== "undefined") {
|
|
27
|
+
const initialState = document.body.dataset.kState
|
|
28
|
+
if (initialState) for (const [id, value] of JSON.parse(initialState)) browserState.set(id, value)
|
|
29
|
+
for (const node of document.querySelectorAll("[data-k-text]")) browserState.set(node.dataset.kText, JSON.parse(node.dataset.kValue))
|
|
30
|
+
|
|
31
|
+
const eventNames = ["click", "input", "change"]
|
|
32
|
+
for (const eventName of eventNames) document.addEventListener(eventName, event => {
|
|
33
|
+
const target = event.target.closest(`[data-k-on-${eventName}]`)
|
|
34
|
+
if (target) applyCommands(browserState, JSON.parse(target.getAttribute(`data-k-on-${eventName}`)), commitDom)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { deserialize } from "./serialization.js"
|
|
2
2
|
|
|
3
|
-
export function createEffectContext(state, stateIds, commit, serializedScope = {}) {
|
|
3
|
+
export function createEffectContext(state, stateIds, commit, serializedScope = {}, active = () => true) {
|
|
4
4
|
const changed = new Set()
|
|
5
5
|
let scheduled = false
|
|
6
6
|
|
|
7
7
|
const flush = () => {
|
|
8
8
|
scheduled = false
|
|
9
|
+
if (!active()) return changed.clear()
|
|
9
10
|
const ids = [...changed]
|
|
10
11
|
changed.clear()
|
|
11
12
|
for (const id of ids) commit(id, state.get(id))
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const setId = (id, value) => {
|
|
16
|
+
if (!active()) return
|
|
15
17
|
const current = state.get(id)
|
|
16
18
|
state.set(id, typeof value === "function" ? value(current) : value)
|
|
17
19
|
changed.add(id)
|
|
@@ -7,7 +7,7 @@ const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
|
|
|
7
7
|
const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
|
|
8
8
|
const itemParts = new WeakMap()
|
|
9
9
|
const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
10
|
-
const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}`
|
|
10
|
+
const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}${__KUDZU_LIST_EFFECTS__ ? ",[data-k-effects]" : ""}`
|
|
11
11
|
|
|
12
12
|
function commitLists(id) {
|
|
13
13
|
const lists = listTargets.get(id)
|
|
@@ -114,8 +114,11 @@ function updateList(list) {
|
|
|
114
114
|
} else node.remove()
|
|
115
115
|
}
|
|
116
116
|
if (added) {
|
|
117
|
-
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount)
|
|
118
|
-
|
|
117
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) {
|
|
118
|
+
const addedNodes = [...additions.childNodes]
|
|
119
|
+
parent.insertBefore(additions, list.boundary)
|
|
120
|
+
for (const node of addedNodes) mountDom(node)
|
|
121
|
+
} else parent.insertBefore(additions, list.boundary)
|
|
119
122
|
list.container ??= parent
|
|
120
123
|
}
|
|
121
124
|
let anchor = list.boundary
|
|
@@ -149,6 +152,7 @@ function fillListItem(root, item) {
|
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
function fillListParts(root, parts, item, revision) {
|
|
155
|
+
if (__KUDZU_LIST_EFFECTS__) for (const node of parts.effects) node.dataset.kEffectItem = JSON.stringify(item)
|
|
152
156
|
for (const [node, field] of parts.directTexts) {
|
|
153
157
|
const text = item?.[field]
|
|
154
158
|
const value = text == null ? "" : String(text)
|
|
@@ -203,7 +207,7 @@ function fillListParts(root, parts, item, revision) {
|
|
|
203
207
|
function listItemParts(root) {
|
|
204
208
|
let parts = itemParts.get(root)
|
|
205
209
|
if (parts) return parts
|
|
206
|
-
parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [], conditions: [] }
|
|
210
|
+
parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [], conditions: [], effects: [] }
|
|
207
211
|
for (const node of matching(root, itemPartsSelector)) {
|
|
208
212
|
if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
|
|
209
213
|
if (__KUDZU_LIST_ATTRIBUTES__ && node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
|
|
@@ -214,6 +218,7 @@ function listItemParts(root) {
|
|
|
214
218
|
parts.conditions.push([node, JSON.parse(node.dataset.kListCondition)])
|
|
215
219
|
conditionOwners.set(node, root)
|
|
216
220
|
}
|
|
221
|
+
if (__KUDZU_LIST_EFFECTS__ && node.hasAttribute("data-k-effects")) parts.effects.push(node)
|
|
217
222
|
}
|
|
218
223
|
itemParts.set(root, parts)
|
|
219
224
|
return parts
|
|
@@ -230,7 +235,8 @@ function listItemPartPlan(template) {
|
|
|
230
235
|
events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [indexes.get(node), events]) : [],
|
|
231
236
|
expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
|
|
232
237
|
expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
|
|
233
|
-
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : []
|
|
238
|
+
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
|
|
239
|
+
effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(node => indexes.get(node)) : []
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
|
|
@@ -246,7 +252,8 @@ function mapListItemParts(parts, root) {
|
|
|
246
252
|
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([index, descriptor]) => {
|
|
247
253
|
conditionOwners.set(target[index], root)
|
|
248
254
|
return [target[index], descriptor]
|
|
249
|
-
}) : []
|
|
255
|
+
}) : [],
|
|
256
|
+
effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(index => target[index]) : []
|
|
250
257
|
})
|
|
251
258
|
}
|
|
252
259
|
|