@kudzujs/core 0.4.6 → 0.4.7
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 +3 -1
- package/framework/binding-runtime.js +6 -1
- package/framework/build.mjs +3 -1
- package/framework/core.mjs +9 -12
- package/framework/style.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,6 +108,7 @@ The handler above increments by two and patches its bound DOM once. Inspect the
|
|
|
108
108
|
<input value={name} onInput={event => setName(event.currentTarget.value)} />
|
|
109
109
|
<input type="checkbox" checked={subscribed} onChange={event => setSubscribed(event.currentTarget.checked)} />
|
|
110
110
|
<select value={theme} onChange={event => setTheme(event.currentTarget.value)} />
|
|
111
|
+
<div style={{ opacity: open ? 1 : 0, width: open ? 240 : 0 }} />
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
Regular attributes use the same expressions without an allowlist:
|
|
@@ -121,7 +122,7 @@ Regular attributes use the same expressions without an allowlist:
|
|
|
121
122
|
/>
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
Kudzu compiles derived expressions to external ESM and patches only the bound DOM attribute or property. `aria-*` and `data-*` boolean values serialize as `"true"` or `"false"`; ordinary false values remove the attribute.
|
|
125
|
+
Kudzu compiles derived expressions to external ESM and patches only the bound DOM attribute or property. `aria-*` and `data-*` boolean values serialize as `"true"` or `"false"`; ordinary false values remove the attribute. Object `style` values use React-shaped camelCase properties, add `px` to nonzero dimensional numbers, and preserve unitless properties and CSS custom properties. Reactive `ref` and `dangerouslySetInnerHTML` remain unsupported.
|
|
125
126
|
|
|
126
127
|
## Conditional DOM
|
|
127
128
|
|
|
@@ -228,6 +229,7 @@ Supported:
|
|
|
228
229
|
- Serializable component-local captures
|
|
229
230
|
- Direct text DOM patches
|
|
230
231
|
- Reactive standard, `aria-*`, and `data-*` attributes
|
|
232
|
+
- Reactive object `style` attributes
|
|
231
233
|
- Controlled `value` and `checked` form properties
|
|
232
234
|
- Conditional child `&&` and ternary DOM patches
|
|
233
235
|
- Direct keyed local-state lists
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { browserState, mountDom, registerCommitter, registerMountHook, registerUnmountHook, unmountDom } from "./shared-runtime.js"
|
|
2
2
|
import { deserialize } from "./serialization.js"
|
|
3
|
+
import { serializeStyle } from "./style.js"
|
|
3
4
|
|
|
4
5
|
const imports = new Map()
|
|
5
6
|
const bindingTargets = new Map()
|
|
@@ -8,7 +9,7 @@ const mountedBindings = new WeakSet()
|
|
|
8
9
|
const mountedConditions = new WeakSet()
|
|
9
10
|
const bindingRegistrations = new WeakMap()
|
|
10
11
|
const conditionRegistrations = new WeakMap()
|
|
11
|
-
const bindingTypes = ["class", "disabled", "value", "checked"]
|
|
12
|
+
const bindingTypes = ["class", "disabled", "value", "checked", "style"]
|
|
12
13
|
const bindingSelector = [...bindingTypes.map(target => `[data-k-bind-${target}]`), "[data-k-bind-attrs]"].join(",")
|
|
13
14
|
|
|
14
15
|
export function patchBinding(node, target, value) {
|
|
@@ -19,6 +20,10 @@ export function patchBinding(node, target, value) {
|
|
|
19
20
|
} else if (target === "value") {
|
|
20
21
|
const next = value == null ? "" : String(value)
|
|
21
22
|
if (node.value !== next) node.value = next
|
|
23
|
+
} else if (target === "style") {
|
|
24
|
+
const style = serializeStyle(value)
|
|
25
|
+
if (style) node.setAttribute("style", style)
|
|
26
|
+
else node.removeAttribute("style")
|
|
22
27
|
} else if (target === "class" && (value == null || value === false)) {
|
|
23
28
|
node.removeAttribute("class")
|
|
24
29
|
} else if (target === "class") {
|
package/framework/build.mjs
CHANGED
|
@@ -73,9 +73,11 @@ export async function build({ quiet = false } = {}) {
|
|
|
73
73
|
}
|
|
74
74
|
if (bindingCount || hasNativeHandlers) await cp(new URL("./serialization.js", import.meta.url), join(assetsDirectory, "kudzu-serialization.js"))
|
|
75
75
|
if (bindingCount) {
|
|
76
|
+
await cp(new URL("./style.js", import.meta.url), join(assetsDirectory, "kudzu-style.js"))
|
|
76
77
|
const bindingRuntime = (await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"))
|
|
77
78
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
78
79
|
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
80
|
+
.replace('"./style.js"', '"./kudzu-style.js"')
|
|
79
81
|
await writeFile(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime)
|
|
80
82
|
}
|
|
81
83
|
if (listCount) {
|
|
@@ -388,7 +390,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
|
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
392
|
|
|
391
|
-
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !/^on/i.test(node.name.getText()) && !["
|
|
393
|
+
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !/^on/i.test(node.name.getText()) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.getText().toLowerCase())) {
|
|
392
394
|
const expression = node.initializer.expression
|
|
393
395
|
const setters = settersForNode(node, settersByFunction)
|
|
394
396
|
const usedStates = referencedStateNames(expression, setters)
|
package/framework/core.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { serializeStyle } from "./style.js"
|
|
2
|
+
|
|
1
3
|
const signalMarker = Symbol("kudzu.signal")
|
|
2
4
|
const behaviorMarker = Symbol("kudzu.behavior")
|
|
3
5
|
const nativeBehaviorMarker = Symbol("kudzu.nativeBehavior")
|
|
@@ -352,7 +354,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
352
354
|
if (rawName === "selected" && selectValue !== noSelectValue) continue
|
|
353
355
|
if (/^on/i.test(rawName) && !/^on[A-Z]/.test(rawName)) throw new Error(`${rawName} must use a camelCase event handler`)
|
|
354
356
|
if (rawName.toLowerCase().startsWith("data-k-")) throw new Error(`${rawName} uses Kudzu's reserved data-k-* prefix`)
|
|
355
|
-
if (["
|
|
357
|
+
if (["ref", "dangerouslysetinnerhtml"].includes(rawName.toLowerCase()) && (value?.[signalMarker] || value?.[bindingMarker])) {
|
|
356
358
|
throw new Error(`Reactive ${rawName} is not supported`)
|
|
357
359
|
}
|
|
358
360
|
|
|
@@ -377,7 +379,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
377
379
|
}
|
|
378
380
|
|
|
379
381
|
const name = rawName === "className" ? "class" : rawName === "htmlFor" ? "for" : rawName
|
|
380
|
-
const propertyTarget = name === "class" || name === "disabled" || name === "value" || name === "checked"
|
|
382
|
+
const propertyTarget = name === "class" || name === "disabled" || name === "value" || name === "checked" || name === "style"
|
|
381
383
|
if (value?.[listFieldMarker]) {
|
|
382
384
|
attributes += renderAttribute(name, value.value)
|
|
383
385
|
listAttributes.push([name, value.field])
|
|
@@ -409,12 +411,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
409
411
|
}
|
|
410
412
|
|
|
411
413
|
if (tag === "select" && name === "value") continue
|
|
412
|
-
|
|
413
|
-
const style = Object.entries(value).map(([property, entry]) => `${toKebabCase(property)}:${entry}`).join(";")
|
|
414
|
-
attributes += ` style="${escapeAttribute(style)}"`
|
|
415
|
-
} else {
|
|
416
|
-
attributes += renderAttribute(name, value)
|
|
417
|
-
}
|
|
414
|
+
attributes += renderAttribute(name, value)
|
|
418
415
|
}
|
|
419
416
|
|
|
420
417
|
if (attributeBindings.length) attributes += ` data-k-bind-attrs='${escapeJsonAttribute(attributeBindings)}'`
|
|
@@ -481,6 +478,10 @@ function reactiveStateIds(descriptor) {
|
|
|
481
478
|
}
|
|
482
479
|
|
|
483
480
|
function renderAttribute(name, value) {
|
|
481
|
+
if (name === "style") {
|
|
482
|
+
const style = serializeStyle(value)
|
|
483
|
+
return style ? ` style="${escapeAttribute(style)}"` : ""
|
|
484
|
+
}
|
|
484
485
|
if (name === "disabled" || name === "checked") return value ? ` ${name}` : ""
|
|
485
486
|
if (name === "value") return value == null ? "" : ` value="${escapeAttribute(value)}"`
|
|
486
487
|
if (value == null || (value === false && !isStringBooleanAttribute(name))) return ""
|
|
@@ -525,7 +526,3 @@ function listSeed(items, fields) {
|
|
|
525
526
|
}
|
|
526
527
|
return seed
|
|
527
528
|
}
|
|
528
|
-
|
|
529
|
-
function toKebabCase(value) {
|
|
530
|
-
return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`)
|
|
531
|
-
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const unitless = new Set([
|
|
2
|
+
"animationIterationCount", "aspectRatio", "borderImageOutset", "borderImageSlice", "borderImageWidth",
|
|
3
|
+
"columnCount", "columns", "flex", "flexGrow", "flexShrink", "fontWeight", "gridArea", "gridColumn",
|
|
4
|
+
"gridColumnEnd", "gridColumnSpan", "gridColumnStart", "gridRow", "gridRowEnd", "gridRowSpan", "gridRowStart",
|
|
5
|
+
"lineClamp", "lineHeight", "opacity", "order", "orphans", "scale", "tabSize", "widows", "zIndex", "zoom",
|
|
6
|
+
"fillOpacity", "floodOpacity", "stopOpacity", "strokeDasharray", "strokeDashoffset", "strokeMiterlimit",
|
|
7
|
+
"strokeOpacity", "strokeWidth"
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
export function serializeStyle(value) {
|
|
11
|
+
if (value == null || value === false) return ""
|
|
12
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new Error("style must be an object")
|
|
13
|
+
return Object.entries(value).flatMap(([property, entry]) => {
|
|
14
|
+
if (entry == null || typeof entry === "boolean") return []
|
|
15
|
+
if (typeof entry === "number" && !Number.isFinite(entry)) throw new Error(`style.${property} must be finite`)
|
|
16
|
+
const suffix = typeof entry === "number" && entry !== 0 && !property.startsWith("--") && !unitless.has(property) ? "px" : ""
|
|
17
|
+
return `${toKebabCase(property)}:${entry}${suffix}`
|
|
18
|
+
}).join(";")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toKebabCase(value) {
|
|
22
|
+
return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`).replace(/^ms-/, "-ms-")
|
|
23
|
+
}
|