@pyreon/runtime-dom 0.5.4 → 0.5.6
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +69 -31
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +50 -30
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/hydrate.ts +1 -1
- package/src/mount.ts +48 -6
- package/src/nodes.ts +9 -3
- package/src/props.ts +47 -29
- package/src/tests/coverage.test.ts +42 -0
- package/src/tests/mount.test.ts +17 -1
- package/src/transition-group.ts +0 -1
package/src/props.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Props } from "@pyreon/core"
|
|
2
|
+
import { normalizeStyleValue, toKebabCase } from "@pyreon/core"
|
|
3
|
+
|
|
2
4
|
import { batch, renderEffect } from "@pyreon/reactivity"
|
|
3
5
|
import { DELEGATED_EVENTS, delegatedPropName } from "./delegate"
|
|
4
6
|
|
|
@@ -187,23 +189,26 @@ const EVENT_RE = /^on[A-Z]/
|
|
|
187
189
|
* Uses for-in instead of Object.keys() to avoid allocating a keys array.
|
|
188
190
|
*/
|
|
189
191
|
export function applyProps(el: Element, props: Props): Cleanup | null {
|
|
190
|
-
let
|
|
192
|
+
let first: Cleanup | null = null
|
|
193
|
+
let cleanups: Cleanup[] | null = null
|
|
191
194
|
for (const key in props) {
|
|
192
195
|
if (key === "key" || key === "ref") continue
|
|
193
196
|
const c = applyProp(el, key, props[key])
|
|
194
197
|
if (c) {
|
|
195
|
-
if (!
|
|
196
|
-
|
|
198
|
+
if (!first) {
|
|
199
|
+
first = c
|
|
200
|
+
} else if (!cleanups) {
|
|
201
|
+
cleanups = [first, c]
|
|
197
202
|
} else {
|
|
198
|
-
|
|
199
|
-
cleanup = () => {
|
|
200
|
-
prev()
|
|
201
|
-
c()
|
|
202
|
-
}
|
|
203
|
+
cleanups.push(c)
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
|
-
|
|
207
|
+
if (cleanups)
|
|
208
|
+
return () => {
|
|
209
|
+
for (const c of cleanups) c()
|
|
210
|
+
}
|
|
211
|
+
return first
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
/**
|
|
@@ -213,28 +218,37 @@ export function applyProps(el: Element, props: Props): Cleanup | null {
|
|
|
213
218
|
* - `() => value` (non-event function) → reactive via effect
|
|
214
219
|
* - anything else → static attribute / DOM property
|
|
215
220
|
*/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Bind an event handler (onClick → "click") with batching + delegation support.
|
|
223
|
+
*/
|
|
224
|
+
function applyEventProp(el: Element, key: string, value: unknown): Cleanup | null {
|
|
225
|
+
if (__DEV__ && typeof value !== "function") {
|
|
226
|
+
console.warn(
|
|
227
|
+
`[Pyreon] Event handler "${key}" received a non-function value (${typeof value}). ` +
|
|
228
|
+
`Expected a function. Did you mean ${key}={() => ...}?`,
|
|
229
|
+
)
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
const eventName = key[2]?.toLowerCase() + key.slice(3)
|
|
233
|
+
const handler = value as EventListener
|
|
234
|
+
|
|
235
|
+
if (DELEGATED_EVENTS.has(eventName)) {
|
|
236
|
+
const prop = delegatedPropName(eventName)
|
|
237
|
+
;(el as unknown as Record<string, unknown>)[prop] = (e: Event) => batch(() => handler(e))
|
|
238
|
+
return () => {
|
|
239
|
+
;(el as unknown as Record<string, unknown>)[prop] = undefined
|
|
231
240
|
}
|
|
232
|
-
|
|
233
|
-
const batched: EventListener = (e) => batch(() => handler(e))
|
|
234
|
-
el.addEventListener(eventName, batched)
|
|
235
|
-
return () => el.removeEventListener(eventName, batched)
|
|
236
241
|
}
|
|
237
242
|
|
|
243
|
+
const batched: EventListener = (e) => batch(() => handler(e))
|
|
244
|
+
el.addEventListener(eventName, batched)
|
|
245
|
+
return () => el.removeEventListener(eventName, batched)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function applyProp(el: Element, key: string, value: unknown): Cleanup | null {
|
|
249
|
+
// Event listener: onClick → "click"
|
|
250
|
+
if (EVENT_RE.test(key)) return applyEventProp(el, key, value)
|
|
251
|
+
|
|
238
252
|
// innerHTML — sanitized via Sanitizer API or fallback allowlist sanitizer
|
|
239
253
|
if (key === "innerHTML") {
|
|
240
254
|
if (typeof (el as HTMLElement & { setHTML?: (h: string) => void }).setHTML === "function") {
|
|
@@ -297,7 +311,11 @@ function applyStyleProp(el: HTMLElement, value: unknown): void {
|
|
|
297
311
|
if (typeof value === "string") {
|
|
298
312
|
el.style.cssText = value
|
|
299
313
|
} else if (value != null && typeof value === "object") {
|
|
300
|
-
|
|
314
|
+
const obj = value as Record<string, unknown>
|
|
315
|
+
for (const k in obj) {
|
|
316
|
+
const css = normalizeStyleValue(k, obj[k])
|
|
317
|
+
el.style.setProperty(k.startsWith("--") ? k : toKebabCase(k), css)
|
|
318
|
+
}
|
|
301
319
|
}
|
|
302
320
|
}
|
|
303
321
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
onMount,
|
|
14
14
|
onUnmount,
|
|
15
15
|
onUpdate,
|
|
16
|
+
Portal,
|
|
16
17
|
} from "@pyreon/core"
|
|
17
18
|
import { signal } from "@pyreon/reactivity"
|
|
18
19
|
import { installDevTools, registerComponent, unregisterComponent } from "../devtools"
|
|
@@ -360,6 +361,37 @@ describe("mount.ts — uncovered branches", () => {
|
|
|
360
361
|
warnSpy.mockRestore()
|
|
361
362
|
})
|
|
362
363
|
|
|
364
|
+
test("component returning Promise triggers dev warning", () => {
|
|
365
|
+
const el = container()
|
|
366
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
367
|
+
|
|
368
|
+
const AsyncComp = (() => Promise.resolve(null)) as unknown as ComponentFn
|
|
369
|
+
mount(h(AsyncComp, null), el)
|
|
370
|
+
|
|
371
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("returned a Promise"))
|
|
372
|
+
warnSpy.mockRestore()
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test("void element with children triggers dev warning", () => {
|
|
376
|
+
const el = container()
|
|
377
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
378
|
+
|
|
379
|
+
mount(h("img", null, "child text"), el)
|
|
380
|
+
|
|
381
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("void element"))
|
|
382
|
+
warnSpy.mockRestore()
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test("Portal with falsy target warns", () => {
|
|
386
|
+
const el = container()
|
|
387
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
388
|
+
|
|
389
|
+
mount(h(Portal, { target: null }), el)
|
|
390
|
+
|
|
391
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Portal"))
|
|
392
|
+
warnSpy.mockRestore()
|
|
393
|
+
})
|
|
394
|
+
|
|
363
395
|
test("component subtree mount error with propagateError (lines 298-309)", () => {
|
|
364
396
|
const el = container()
|
|
365
397
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
@@ -914,6 +946,16 @@ describe("props.ts — uncovered branches", () => {
|
|
|
914
946
|
unmount()
|
|
915
947
|
})
|
|
916
948
|
|
|
949
|
+
test("non-function event handler triggers dev warning", () => {
|
|
950
|
+
const el = container()
|
|
951
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
|
952
|
+
|
|
953
|
+
mount(h("button", { onClick: "not a function" }), el)
|
|
954
|
+
|
|
955
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("non-function value"))
|
|
956
|
+
warnSpy.mockRestore()
|
|
957
|
+
})
|
|
958
|
+
|
|
917
959
|
test("n-show prop toggles display (lines 273-277)", () => {
|
|
918
960
|
const el = container()
|
|
919
961
|
const visible = signal(true)
|
package/src/tests/mount.test.ts
CHANGED
|
@@ -975,6 +975,23 @@ describe("mount — props (extended)", () => {
|
|
|
975
975
|
expect(div.style.marginTop).toBe("10px")
|
|
976
976
|
})
|
|
977
977
|
|
|
978
|
+
test("style object auto-appends px to numeric values", () => {
|
|
979
|
+
const el = container()
|
|
980
|
+
mount(h("div", { style: { height: 100, marginTop: 20, opacity: 0.5, zIndex: 10 } }), el)
|
|
981
|
+
const div = el.querySelector("div") as HTMLElement
|
|
982
|
+
expect(div.style.height).toBe("100px")
|
|
983
|
+
expect(div.style.marginTop).toBe("20px")
|
|
984
|
+
expect(div.style.opacity).toBe("0.5")
|
|
985
|
+
expect(div.style.zIndex).toBe("10")
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
test("style object handles CSS custom properties", () => {
|
|
989
|
+
const el = container()
|
|
990
|
+
mount(h("div", { style: { "--my-color": "red" } }), el)
|
|
991
|
+
const div = el.querySelector("div") as HTMLElement
|
|
992
|
+
expect(div.style.getPropertyValue("--my-color")).toBe("red")
|
|
993
|
+
})
|
|
994
|
+
|
|
978
995
|
test("className sets class attribute", () => {
|
|
979
996
|
const el = container()
|
|
980
997
|
mount(h("div", { className: "my-class" }), el)
|
|
@@ -1841,7 +1858,6 @@ describe("hydrateRoot — extended", () => {
|
|
|
1841
1858
|
const Comp = defineComponent(() => {
|
|
1842
1859
|
onMount(() => {
|
|
1843
1860
|
mountCalled = true
|
|
1844
|
-
return undefined
|
|
1845
1861
|
})
|
|
1846
1862
|
return h("span", null, "mounted")
|
|
1847
1863
|
})
|
package/src/transition-group.ts
CHANGED