@fictjs/runtime 0.0.8 → 0.0.10
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/dist/index.cjs +2542 -2457
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -26
- package/dist/index.d.ts +30 -26
- package/dist/index.dev.js +2583 -2495
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +2542 -2457
- package/dist/index.js.map +1 -1
- package/dist/slim.cjs +2123 -1988
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.d.cts +13 -11
- package/dist/slim.d.ts +13 -11
- package/dist/slim.js +2123 -1988
- package/dist/slim.js.map +1 -1
- package/package.json +1 -1
- package/src/binding.ts +115 -92
- package/src/dom.ts +90 -38
- package/src/effect.ts +2 -1
- package/src/error-boundary.ts +3 -1
- package/src/hooks.ts +14 -1
- package/src/list-helpers.ts +141 -50
- package/src/ref.ts +1 -1
- package/src/signal.ts +16 -2
- package/src/store.ts +30 -2
- package/src/types.ts +3 -3
package/package.json
CHANGED
package/src/binding.ts
CHANGED
|
@@ -36,7 +36,9 @@ import {
|
|
|
36
36
|
registerRootCleanup,
|
|
37
37
|
type RootContext,
|
|
38
38
|
} from './lifecycle'
|
|
39
|
+
import { createVersionedSignalAccessor } from './list-helpers'
|
|
39
40
|
import { toNodeArray, removeNodes, insertNodesBefore } from './node-ops'
|
|
41
|
+
import { batch } from './scheduler'
|
|
40
42
|
import { computed, createSignal, untrack, type Signal } from './signal'
|
|
41
43
|
import type { Cleanup, FictNode } from './types'
|
|
42
44
|
|
|
@@ -66,10 +68,8 @@ interface ManagedBlock<T = unknown> {
|
|
|
66
68
|
root: RootContext
|
|
67
69
|
value: Signal<T>
|
|
68
70
|
index: Signal<number>
|
|
69
|
-
version: Signal<number>
|
|
70
71
|
start: Comment
|
|
71
72
|
end: Comment
|
|
72
|
-
valueProxy: T
|
|
73
73
|
renderCurrent: () => FictNode
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -165,7 +165,7 @@ export function unwrapPrimitive<T>(value: T): T {
|
|
|
165
165
|
return value
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
function
|
|
168
|
+
function _createValueProxy<T>(read: () => T): T {
|
|
169
169
|
const getPrimitivePrototype = (value: unknown): Record<PropertyKey, unknown> | undefined => {
|
|
170
170
|
switch (typeof value) {
|
|
171
171
|
case 'string':
|
|
@@ -339,7 +339,7 @@ function formatTextValue(value: unknown): string {
|
|
|
339
339
|
// ============================================================================
|
|
340
340
|
|
|
341
341
|
/** Attribute setter function type */
|
|
342
|
-
export type AttributeSetter = (el:
|
|
342
|
+
export type AttributeSetter = (el: Element, key: string, value: unknown) => void
|
|
343
343
|
|
|
344
344
|
/**
|
|
345
345
|
* Create a reactive attribute binding on an element.
|
|
@@ -354,7 +354,7 @@ export type AttributeSetter = (el: HTMLElement, key: string, value: unknown) =>
|
|
|
354
354
|
* ```
|
|
355
355
|
*/
|
|
356
356
|
export function createAttributeBinding(
|
|
357
|
-
el:
|
|
357
|
+
el: Element,
|
|
358
358
|
key: string,
|
|
359
359
|
value: MaybeReactive<unknown>,
|
|
360
360
|
setter: AttributeSetter,
|
|
@@ -373,7 +373,7 @@ export function createAttributeBinding(
|
|
|
373
373
|
/**
|
|
374
374
|
* Bind a reactive value to an element's attribute.
|
|
375
375
|
*/
|
|
376
|
-
export function bindAttribute(el:
|
|
376
|
+
export function bindAttribute(el: Element, key: string, getValue: () => unknown): Cleanup {
|
|
377
377
|
let prevValue: unknown = undefined
|
|
378
378
|
return createRenderEffect(() => {
|
|
379
379
|
const value = getValue()
|
|
@@ -393,7 +393,7 @@ export function bindAttribute(el: HTMLElement, key: string, getValue: () => unkn
|
|
|
393
393
|
/**
|
|
394
394
|
* Bind a reactive value to an element's property.
|
|
395
395
|
*/
|
|
396
|
-
export function bindProperty(el:
|
|
396
|
+
export function bindProperty(el: Element, key: string, getValue: () => unknown): Cleanup {
|
|
397
397
|
// Keep behavior aligned with the legacy createElement+applyProps path in `dom.ts`,
|
|
398
398
|
// where certain keys must behave like DOM properties and nullish clears should
|
|
399
399
|
// reset to sensible defaults (e.g. value -> '', checked -> false).
|
|
@@ -430,18 +430,19 @@ export function bindProperty(el: HTMLElement, key: string, getValue: () => unkno
|
|
|
430
430
|
* Apply styles to an element, supporting reactive style objects/strings.
|
|
431
431
|
*/
|
|
432
432
|
export function createStyleBinding(
|
|
433
|
-
el:
|
|
433
|
+
el: Element,
|
|
434
434
|
value: MaybeReactive<string | Record<string, string | number> | null | undefined>,
|
|
435
435
|
): void {
|
|
436
|
+
const target = el as Element & { style: CSSStyleDeclaration }
|
|
436
437
|
if (isReactive(value)) {
|
|
437
438
|
let prev: unknown
|
|
438
439
|
createRenderEffect(() => {
|
|
439
440
|
const next = (value as () => unknown)()
|
|
440
|
-
applyStyle(
|
|
441
|
+
applyStyle(target, next, prev)
|
|
441
442
|
prev = next
|
|
442
443
|
})
|
|
443
444
|
} else {
|
|
444
|
-
applyStyle(
|
|
445
|
+
applyStyle(target, value, undefined)
|
|
445
446
|
}
|
|
446
447
|
}
|
|
447
448
|
|
|
@@ -449,13 +450,14 @@ export function createStyleBinding(
|
|
|
449
450
|
* Bind a reactive style value to an existing element.
|
|
450
451
|
*/
|
|
451
452
|
export function bindStyle(
|
|
452
|
-
el:
|
|
453
|
+
el: Element,
|
|
453
454
|
getValue: () => string | Record<string, string | number> | null | undefined,
|
|
454
455
|
): Cleanup {
|
|
456
|
+
const target = el as Element & { style: CSSStyleDeclaration }
|
|
455
457
|
let prev: unknown
|
|
456
458
|
return createRenderEffect(() => {
|
|
457
459
|
const next = getValue()
|
|
458
|
-
applyStyle(
|
|
460
|
+
applyStyle(target, next, prev)
|
|
459
461
|
prev = next
|
|
460
462
|
})
|
|
461
463
|
}
|
|
@@ -463,7 +465,11 @@ export function bindStyle(
|
|
|
463
465
|
/**
|
|
464
466
|
* Apply a style value to an element
|
|
465
467
|
*/
|
|
466
|
-
function applyStyle(
|
|
468
|
+
function applyStyle(
|
|
469
|
+
el: Element & { style: CSSStyleDeclaration },
|
|
470
|
+
value: unknown,
|
|
471
|
+
prev: unknown,
|
|
472
|
+
): void {
|
|
467
473
|
if (typeof value === 'string') {
|
|
468
474
|
el.style.cssText = value
|
|
469
475
|
} else if (value && typeof value === 'object') {
|
|
@@ -525,7 +531,7 @@ function isUnitlessStyleProperty(prop: string): boolean {
|
|
|
525
531
|
* Apply class to an element, supporting reactive class values.
|
|
526
532
|
*/
|
|
527
533
|
export function createClassBinding(
|
|
528
|
-
el:
|
|
534
|
+
el: Element,
|
|
529
535
|
value: MaybeReactive<string | Record<string, boolean> | null | undefined>,
|
|
530
536
|
): void {
|
|
531
537
|
if (isReactive(value)) {
|
|
@@ -543,12 +549,22 @@ export function createClassBinding(
|
|
|
543
549
|
* Bind a reactive class value to an existing element.
|
|
544
550
|
*/
|
|
545
551
|
export function bindClass(
|
|
546
|
-
el:
|
|
552
|
+
el: Element,
|
|
547
553
|
getValue: () => string | Record<string, boolean> | null | undefined,
|
|
548
554
|
): Cleanup {
|
|
549
555
|
let prev: Record<string, boolean> = {}
|
|
556
|
+
let prevString: string | undefined
|
|
550
557
|
return createRenderEffect(() => {
|
|
551
558
|
const next = getValue()
|
|
559
|
+
// P2-1: Short-circuit for string values to avoid DOM writes when unchanged
|
|
560
|
+
if (typeof next === 'string') {
|
|
561
|
+
if (next === prevString) return
|
|
562
|
+
prevString = next
|
|
563
|
+
el.className = next
|
|
564
|
+
prev = {}
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
prevString = undefined
|
|
552
568
|
prev = applyClass(el, next, prev)
|
|
553
569
|
})
|
|
554
570
|
}
|
|
@@ -556,7 +572,7 @@ export function bindClass(
|
|
|
556
572
|
/**
|
|
557
573
|
* Toggle a class key (supports space-separated class names)
|
|
558
574
|
*/
|
|
559
|
-
function toggleClassKey(node:
|
|
575
|
+
function toggleClassKey(node: Element, key: string, value: boolean): void {
|
|
560
576
|
const classNames = key.trim().split(/\s+/)
|
|
561
577
|
for (let i = 0, len = classNames.length; i < len; i++) {
|
|
562
578
|
node.classList.toggle(classNames[i]!, value)
|
|
@@ -567,7 +583,7 @@ function toggleClassKey(node: HTMLElement, key: string, value: boolean): void {
|
|
|
567
583
|
* Apply a class value to an element using classList.toggle for efficient updates.
|
|
568
584
|
* Returns the new prev state for tracking.
|
|
569
585
|
*/
|
|
570
|
-
function applyClass(el:
|
|
586
|
+
function applyClass(el: Element, value: unknown, prev: unknown): Record<string, boolean> {
|
|
571
587
|
const prevState = (prev && typeof prev === 'object' ? prev : {}) as Record<string, boolean>
|
|
572
588
|
|
|
573
589
|
// Handle string value - full replacement
|
|
@@ -620,7 +636,7 @@ function applyClass(el: HTMLElement, value: unknown, prev: unknown): Record<stri
|
|
|
620
636
|
* Exported classList function for direct use (compatible with dom-expressions)
|
|
621
637
|
*/
|
|
622
638
|
export function classList(
|
|
623
|
-
node:
|
|
639
|
+
node: Element,
|
|
624
640
|
value: Record<string, boolean> | null | undefined,
|
|
625
641
|
prev: Record<string, boolean> = {},
|
|
626
642
|
): Record<string, boolean> {
|
|
@@ -631,19 +647,6 @@ export function classList(
|
|
|
631
647
|
// Child/Insert Binding (Dynamic Children)
|
|
632
648
|
// ============================================================================
|
|
633
649
|
|
|
634
|
-
/** Managed child node with its dispose function */
|
|
635
|
-
interface ManagedBlock<T = unknown> {
|
|
636
|
-
nodes: Node[]
|
|
637
|
-
root: RootContext
|
|
638
|
-
value: Signal<T>
|
|
639
|
-
index: Signal<number>
|
|
640
|
-
version: Signal<number>
|
|
641
|
-
start: Comment
|
|
642
|
-
end: Comment
|
|
643
|
-
valueProxy: T
|
|
644
|
-
renderCurrent: () => FictNode
|
|
645
|
-
}
|
|
646
|
-
|
|
647
650
|
/**
|
|
648
651
|
* Insert reactive content into a parent element.
|
|
649
652
|
* This is a simpler API than createChildBinding for basic cases.
|
|
@@ -654,11 +657,12 @@ interface ManagedBlock<T = unknown> {
|
|
|
654
657
|
* @param createElementFn - Optional function to create DOM elements (when marker is provided)
|
|
655
658
|
*/
|
|
656
659
|
export function insert(
|
|
657
|
-
parent:
|
|
660
|
+
parent: ParentNode & Node,
|
|
658
661
|
getValue: () => FictNode,
|
|
659
662
|
markerOrCreateElement?: Node | CreateElementFn,
|
|
660
663
|
createElementFn?: CreateElementFn,
|
|
661
664
|
): Cleanup {
|
|
665
|
+
const hostRoot = getCurrentRoot()
|
|
662
666
|
let marker: Node
|
|
663
667
|
let ownsMarker = false
|
|
664
668
|
let createFn: CreateElementFn | undefined = createElementFn
|
|
@@ -736,7 +740,7 @@ export function insert(
|
|
|
736
740
|
}
|
|
737
741
|
clearCurrentNodes()
|
|
738
742
|
|
|
739
|
-
const root = createRootContext()
|
|
743
|
+
const root = createRootContext(hostRoot)
|
|
740
744
|
const prev = pushRoot(root)
|
|
741
745
|
let nodes: Node[] = []
|
|
742
746
|
try {
|
|
@@ -748,7 +752,15 @@ export function insert(
|
|
|
748
752
|
if (value.every(v => v instanceof Node)) {
|
|
749
753
|
newNode = value as Node[]
|
|
750
754
|
} else {
|
|
751
|
-
|
|
755
|
+
if (createFn) {
|
|
756
|
+
const mapped: Node[] = []
|
|
757
|
+
for (const item of value) {
|
|
758
|
+
mapped.push(...toNodeArray(createFn(item as any)))
|
|
759
|
+
}
|
|
760
|
+
newNode = mapped
|
|
761
|
+
} else {
|
|
762
|
+
newNode = document.createTextNode(String(value))
|
|
763
|
+
}
|
|
752
764
|
}
|
|
753
765
|
} else {
|
|
754
766
|
newNode = createFn ? createFn(value) : document.createTextNode(String(value))
|
|
@@ -794,15 +806,16 @@ export function insert(
|
|
|
794
806
|
* ```
|
|
795
807
|
*/
|
|
796
808
|
export function createChildBinding(
|
|
797
|
-
parent:
|
|
809
|
+
parent: ParentNode & Node,
|
|
798
810
|
getValue: () => FictNode,
|
|
799
811
|
createElementFn: CreateElementFn,
|
|
800
812
|
): BindingHandle {
|
|
801
813
|
const marker = document.createComment('fict:child')
|
|
802
814
|
parent.appendChild(marker)
|
|
815
|
+
const hostRoot = getCurrentRoot()
|
|
803
816
|
|
|
804
817
|
const dispose = createRenderEffect(() => {
|
|
805
|
-
const root = createRootContext()
|
|
818
|
+
const root = createRootContext(hostRoot)
|
|
806
819
|
const prev = pushRoot(root)
|
|
807
820
|
let nodes: Node[] = []
|
|
808
821
|
let handledError = false
|
|
@@ -857,10 +870,10 @@ export function createChildBinding(
|
|
|
857
870
|
// Event Delegation System
|
|
858
871
|
// ============================================================================
|
|
859
872
|
|
|
860
|
-
// Extend
|
|
873
|
+
// Extend Element/Document type to support event delegation
|
|
861
874
|
declare global {
|
|
862
|
-
interface
|
|
863
|
-
_$host?:
|
|
875
|
+
interface Element {
|
|
876
|
+
_$host?: Element
|
|
864
877
|
[key: `$$${string}`]: EventListener | [EventListener, unknown] | undefined
|
|
865
878
|
[key: `$$${string}Data`]: unknown
|
|
866
879
|
}
|
|
@@ -912,11 +925,21 @@ export function clearDelegatedEvents(doc: Document = window.document): void {
|
|
|
912
925
|
* Walks up the DOM tree to find and call handlers stored as $$eventName properties.
|
|
913
926
|
*/
|
|
914
927
|
function globalEventHandler(e: Event): void {
|
|
915
|
-
|
|
928
|
+
const asNode = (value: unknown): Node | null =>
|
|
929
|
+
value && typeof (value as Node).nodeType === 'number' ? (value as Node) : null
|
|
930
|
+
const asElement = (value: unknown): Element | null => {
|
|
931
|
+
const n = asNode(value)
|
|
932
|
+
if (!n) return null
|
|
933
|
+
if (n.nodeType === 1) return n as Element
|
|
934
|
+
return (n as ChildNode).parentElement
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
let node = asElement(e.target)
|
|
916
938
|
const key = `$$${e.type}` as const
|
|
917
939
|
const dataKey = `${key}Data` as `$$${string}Data`
|
|
918
940
|
const oriTarget = e.target
|
|
919
941
|
const oriCurrentTarget = e.currentTarget
|
|
942
|
+
let lastHandled: Element | null = null
|
|
920
943
|
|
|
921
944
|
// Retarget helper for shadow DOM and portals
|
|
922
945
|
const retarget = (value: EventTarget) =>
|
|
@@ -945,12 +968,15 @@ function globalEventHandler(e: Event): void {
|
|
|
945
968
|
const rawData = (node as any)[dataKey] as unknown
|
|
946
969
|
const hasData = rawData !== undefined
|
|
947
970
|
const resolvedNodeData = hasData ? resolveData(rawData) : undefined
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
971
|
+
// P2-3: Wrap event handler calls in batch for synchronous flush & reduced microtasks
|
|
972
|
+
batch(() => {
|
|
973
|
+
if (typeof handler === 'function') {
|
|
974
|
+
callEventHandler(handler, e, node, hasData ? resolvedNodeData : undefined)
|
|
975
|
+
} else if (Array.isArray(handler)) {
|
|
976
|
+
const tupleData = resolveData(handler[1])
|
|
977
|
+
callEventHandler(handler[0], e, node, tupleData)
|
|
978
|
+
}
|
|
979
|
+
})
|
|
954
980
|
if (e.cancelBubble) return false
|
|
955
981
|
}
|
|
956
982
|
// Handle shadow DOM host retargeting
|
|
@@ -958,8 +984,11 @@ function globalEventHandler(e: Event): void {
|
|
|
958
984
|
if (
|
|
959
985
|
shadowHost &&
|
|
960
986
|
typeof shadowHost !== 'string' &&
|
|
961
|
-
!(shadowHost as
|
|
962
|
-
|
|
987
|
+
!(shadowHost as Element)._$host &&
|
|
988
|
+
(() => {
|
|
989
|
+
const targetNode = asNode(e.target)
|
|
990
|
+
return targetNode ? node.contains(targetNode) : false
|
|
991
|
+
})()
|
|
963
992
|
) {
|
|
964
993
|
retarget(shadowHost as EventTarget)
|
|
965
994
|
}
|
|
@@ -969,9 +998,7 @@ function globalEventHandler(e: Event): void {
|
|
|
969
998
|
// Walk up tree helper
|
|
970
999
|
const walkUpTree = (): void => {
|
|
971
1000
|
while (handleNode() && node) {
|
|
972
|
-
node = (node._$host ||
|
|
973
|
-
node.parentNode ||
|
|
974
|
-
(node as unknown as ShadowRoot).host) as HTMLElement | null
|
|
1001
|
+
node = asElement(node._$host || node.parentNode || (node as unknown as ShadowRoot).host)
|
|
975
1002
|
}
|
|
976
1003
|
}
|
|
977
1004
|
|
|
@@ -988,8 +1015,11 @@ function globalEventHandler(e: Event): void {
|
|
|
988
1015
|
const path = e.composedPath()
|
|
989
1016
|
retarget(path[0] as EventTarget)
|
|
990
1017
|
for (let i = 0; i < path.length - 2; i++) {
|
|
991
|
-
|
|
1018
|
+
const nextNode = asElement(path[i] as EventTarget)
|
|
1019
|
+
if (!nextNode || nextNode === lastHandled) continue
|
|
1020
|
+
node = nextNode
|
|
992
1021
|
if (!handleNode()) break
|
|
1022
|
+
lastHandled = node
|
|
993
1023
|
// Handle portal event bubbling
|
|
994
1024
|
if (node._$host) {
|
|
995
1025
|
node = node._$host
|
|
@@ -1020,7 +1050,7 @@ function globalEventHandler(e: Event): void {
|
|
|
1020
1050
|
* @param delegate - Whether to use delegation (auto-detected based on event name)
|
|
1021
1051
|
*/
|
|
1022
1052
|
export function addEventListener(
|
|
1023
|
-
node:
|
|
1053
|
+
node: Element,
|
|
1024
1054
|
name: string,
|
|
1025
1055
|
handler: EventListener | [EventListener, unknown] | null | undefined,
|
|
1026
1056
|
delegate?: boolean,
|
|
@@ -1066,7 +1096,7 @@ export function addEventListener(
|
|
|
1066
1096
|
* ```
|
|
1067
1097
|
*/
|
|
1068
1098
|
export function bindEvent(
|
|
1069
|
-
el:
|
|
1099
|
+
el: Element,
|
|
1070
1100
|
eventName: string,
|
|
1071
1101
|
handler: EventListenerOrEventListenerObject | null | undefined,
|
|
1072
1102
|
options?: boolean | AddEventListenerOptions,
|
|
@@ -1153,7 +1183,7 @@ export function bindEvent(
|
|
|
1153
1183
|
* bindRef(el, () => props.ref)
|
|
1154
1184
|
* ```
|
|
1155
1185
|
*/
|
|
1156
|
-
export function bindRef(el:
|
|
1186
|
+
export function bindRef(el: Element, ref: unknown): Cleanup {
|
|
1157
1187
|
if (ref == null) return () => {}
|
|
1158
1188
|
|
|
1159
1189
|
// Handle reactive refs (getters)
|
|
@@ -1164,10 +1194,10 @@ export function bindRef(el: HTMLElement, ref: unknown): Cleanup {
|
|
|
1164
1194
|
|
|
1165
1195
|
if (typeof refValue === 'function') {
|
|
1166
1196
|
// Callback ref: call with element
|
|
1167
|
-
;(refValue as (el:
|
|
1197
|
+
;(refValue as (el: Element) => void)(el)
|
|
1168
1198
|
} else if (typeof refValue === 'object' && 'current' in refValue) {
|
|
1169
1199
|
// Ref object: set current property
|
|
1170
|
-
;(refValue as { current:
|
|
1200
|
+
;(refValue as { current: Element | null }).current = el
|
|
1171
1201
|
}
|
|
1172
1202
|
}
|
|
1173
1203
|
|
|
@@ -1187,7 +1217,7 @@ export function bindRef(el: HTMLElement, ref: unknown): Cleanup {
|
|
|
1187
1217
|
const nullifyCleanup = () => {
|
|
1188
1218
|
const currentRef = getRef()
|
|
1189
1219
|
if (currentRef && typeof currentRef === 'object' && 'current' in currentRef) {
|
|
1190
|
-
;(currentRef as { current:
|
|
1220
|
+
;(currentRef as { current: Element | null }).current = null
|
|
1191
1221
|
}
|
|
1192
1222
|
}
|
|
1193
1223
|
registerRootCleanup(nullifyCleanup)
|
|
@@ -1202,7 +1232,7 @@ export function bindRef(el: HTMLElement, ref: unknown): Cleanup {
|
|
|
1202
1232
|
const cleanup = () => {
|
|
1203
1233
|
const refValue = getRef()
|
|
1204
1234
|
if (refValue && typeof refValue === 'object' && 'current' in refValue) {
|
|
1205
|
-
;(refValue as { current:
|
|
1235
|
+
;(refValue as { current: Element | null }).current = null
|
|
1206
1236
|
}
|
|
1207
1237
|
}
|
|
1208
1238
|
registerRootCleanup(cleanup)
|
|
@@ -1231,7 +1261,7 @@ export function bindRef(el: HTMLElement, ref: unknown): Cleanup {
|
|
|
1231
1261
|
* ```
|
|
1232
1262
|
*/
|
|
1233
1263
|
export function spread(
|
|
1234
|
-
node:
|
|
1264
|
+
node: Element,
|
|
1235
1265
|
props: Record<string, unknown> = {},
|
|
1236
1266
|
isSVG = false,
|
|
1237
1267
|
skipChildren = false,
|
|
@@ -1248,7 +1278,7 @@ export function spread(
|
|
|
1248
1278
|
// Handle ref
|
|
1249
1279
|
createRenderEffect(() => {
|
|
1250
1280
|
if (typeof props.ref === 'function') {
|
|
1251
|
-
;(props.ref as (el:
|
|
1281
|
+
;(props.ref as (el: Element) => void)(node)
|
|
1252
1282
|
}
|
|
1253
1283
|
})
|
|
1254
1284
|
|
|
@@ -1272,7 +1302,7 @@ export function spread(
|
|
|
1272
1302
|
* @param skipRef - Whether to skip ref handling
|
|
1273
1303
|
*/
|
|
1274
1304
|
export function assign(
|
|
1275
|
-
node:
|
|
1305
|
+
node: Element,
|
|
1276
1306
|
props: Record<string, unknown>,
|
|
1277
1307
|
isSVG = false,
|
|
1278
1308
|
skipChildren = false,
|
|
@@ -1307,7 +1337,7 @@ export function assign(
|
|
|
1307
1337
|
* Assign a single prop to a node.
|
|
1308
1338
|
*/
|
|
1309
1339
|
function assignProp(
|
|
1310
|
-
node:
|
|
1340
|
+
node: Element,
|
|
1311
1341
|
prop: string,
|
|
1312
1342
|
value: unknown,
|
|
1313
1343
|
prev: unknown,
|
|
@@ -1317,7 +1347,7 @@ function assignProp(
|
|
|
1317
1347
|
): unknown {
|
|
1318
1348
|
// Style handling
|
|
1319
1349
|
if (prop === 'style') {
|
|
1320
|
-
applyStyle(node, value, prev)
|
|
1350
|
+
applyStyle(node as Element & { style: CSSStyleDeclaration }, value, prev)
|
|
1321
1351
|
return value
|
|
1322
1352
|
}
|
|
1323
1353
|
|
|
@@ -1332,7 +1362,7 @@ function assignProp(
|
|
|
1332
1362
|
// Ref handling
|
|
1333
1363
|
if (prop === 'ref') {
|
|
1334
1364
|
if (!skipRef && typeof value === 'function') {
|
|
1335
|
-
;(value as (el:
|
|
1365
|
+
;(value as (el: Element) => void)(node)
|
|
1336
1366
|
}
|
|
1337
1367
|
return value
|
|
1338
1368
|
}
|
|
@@ -1472,6 +1502,7 @@ export function createConditional(
|
|
|
1472
1502
|
const endMarker = document.createComment('fict:cond:end')
|
|
1473
1503
|
const fragment = document.createDocumentFragment()
|
|
1474
1504
|
fragment.append(startMarker, endMarker)
|
|
1505
|
+
const hostRoot = getCurrentRoot()
|
|
1475
1506
|
|
|
1476
1507
|
let currentNodes: Node[] = []
|
|
1477
1508
|
let currentRoot: RootContext | null = null
|
|
@@ -1513,7 +1544,7 @@ export function createConditional(
|
|
|
1513
1544
|
return
|
|
1514
1545
|
}
|
|
1515
1546
|
|
|
1516
|
-
const root = createRootContext()
|
|
1547
|
+
const root = createRootContext(hostRoot)
|
|
1517
1548
|
const prev = pushRoot(root)
|
|
1518
1549
|
let handledError = false
|
|
1519
1550
|
try {
|
|
@@ -1583,10 +1614,11 @@ export type KeyFn<T> = (item: T, index: number) => string | number
|
|
|
1583
1614
|
|
|
1584
1615
|
/**
|
|
1585
1616
|
* Create a reactive list rendering binding with optional keying.
|
|
1617
|
+
* The render callback receives signal accessors for the item and index.
|
|
1586
1618
|
*/
|
|
1587
1619
|
export function createList<T>(
|
|
1588
1620
|
items: () => T[],
|
|
1589
|
-
renderItem: (item: T
|
|
1621
|
+
renderItem: (item: Signal<T>, index: Signal<number>) => FictNode,
|
|
1590
1622
|
createElementFn: CreateElementFn,
|
|
1591
1623
|
getKey?: KeyFn<T>,
|
|
1592
1624
|
): BindingHandle {
|
|
@@ -1594,6 +1626,7 @@ export function createList<T>(
|
|
|
1594
1626
|
const endMarker = document.createComment('fict:list:end')
|
|
1595
1627
|
const fragment = document.createDocumentFragment()
|
|
1596
1628
|
fragment.append(startMarker, endMarker)
|
|
1629
|
+
const hostRoot = getCurrentRoot()
|
|
1597
1630
|
|
|
1598
1631
|
const nodeMap = new Map<string | number, ManagedBlock<T>>()
|
|
1599
1632
|
let pendingItems: T[] | null = null
|
|
@@ -1621,21 +1654,17 @@ export function createList<T>(
|
|
|
1621
1654
|
if (!getKey && previousValue !== item) {
|
|
1622
1655
|
destroyRoot(existing.root)
|
|
1623
1656
|
removeBlockNodes(existing)
|
|
1624
|
-
block = mountBlock(item, i, renderItem, parent, endMarker, createElementFn)
|
|
1657
|
+
block = mountBlock(item, i, renderItem, parent, endMarker, createElementFn, hostRoot)
|
|
1625
1658
|
} else {
|
|
1626
1659
|
const previousIndex = existing.index()
|
|
1627
1660
|
existing.value(item)
|
|
1628
1661
|
existing.index(i)
|
|
1629
1662
|
|
|
1630
|
-
if (previousValue === item) {
|
|
1631
|
-
bumpBlockVersion(existing)
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
1663
|
const needsRerender = getKey ? true : previousValue !== item || previousIndex !== i
|
|
1635
1664
|
block = needsRerender ? rerenderBlock(existing, createElementFn) : existing
|
|
1636
1665
|
}
|
|
1637
1666
|
} else {
|
|
1638
|
-
block = mountBlock(item, i, renderItem, parent, endMarker, createElementFn)
|
|
1667
|
+
block = mountBlock(item, i, renderItem, parent, endMarker, createElementFn, hostRoot)
|
|
1639
1668
|
}
|
|
1640
1669
|
|
|
1641
1670
|
newNodeMap.set(key, block)
|
|
@@ -1699,7 +1728,11 @@ export function createList<T>(
|
|
|
1699
1728
|
* createShow(container, () => $visible())
|
|
1700
1729
|
* ```
|
|
1701
1730
|
*/
|
|
1702
|
-
export function createShow(
|
|
1731
|
+
export function createShow(
|
|
1732
|
+
el: Element & { style: CSSStyleDeclaration },
|
|
1733
|
+
condition: () => boolean,
|
|
1734
|
+
displayValue?: string,
|
|
1735
|
+
): void {
|
|
1703
1736
|
const originalDisplay = displayValue ?? el.style.display
|
|
1704
1737
|
createRenderEffect(() => {
|
|
1705
1738
|
el.style.display = condition() ? originalDisplay : 'none'
|
|
@@ -1723,7 +1756,7 @@ export function createShow(el: HTMLElement, condition: () => boolean, displayVal
|
|
|
1723
1756
|
* ```
|
|
1724
1757
|
*/
|
|
1725
1758
|
export function createPortal(
|
|
1726
|
-
container:
|
|
1759
|
+
container: ParentNode & Node,
|
|
1727
1760
|
render: () => FictNode,
|
|
1728
1761
|
createElementFn: CreateElementFn,
|
|
1729
1762
|
): BindingHandle {
|
|
@@ -1749,7 +1782,7 @@ export function createPortal(
|
|
|
1749
1782
|
}
|
|
1750
1783
|
|
|
1751
1784
|
// Create new content
|
|
1752
|
-
const root = createRootContext()
|
|
1785
|
+
const root = createRootContext(parentRoot)
|
|
1753
1786
|
const prev = pushRoot(root)
|
|
1754
1787
|
let handledError = false
|
|
1755
1788
|
try {
|
|
@@ -1820,22 +1853,18 @@ export function createPortal(
|
|
|
1820
1853
|
function mountBlock<T>(
|
|
1821
1854
|
initialValue: T,
|
|
1822
1855
|
initialIndex: number,
|
|
1823
|
-
renderItem: (item: T
|
|
1856
|
+
renderItem: (item: Signal<T>, index: Signal<number>) => FictNode,
|
|
1824
1857
|
parent: ParentNode & Node,
|
|
1825
1858
|
anchor: Node,
|
|
1826
1859
|
createElementFn: CreateElementFn,
|
|
1860
|
+
hostRoot?: RootContext | undefined,
|
|
1827
1861
|
): ManagedBlock<T> {
|
|
1828
1862
|
const start = document.createComment('fict:block:start')
|
|
1829
1863
|
const end = document.createComment('fict:block:end')
|
|
1830
|
-
const valueSig =
|
|
1864
|
+
const valueSig = createVersionedSignalAccessor<T>(initialValue)
|
|
1831
1865
|
const indexSig = createSignal<number>(initialIndex)
|
|
1832
|
-
const
|
|
1833
|
-
const
|
|
1834
|
-
versionSig()
|
|
1835
|
-
return valueSig()
|
|
1836
|
-
}) as T
|
|
1837
|
-
const renderCurrent = () => renderItem(valueProxy, indexSig())
|
|
1838
|
-
const root = createRootContext()
|
|
1866
|
+
const renderCurrent = () => renderItem(valueSig, indexSig)
|
|
1867
|
+
const root = createRootContext(hostRoot)
|
|
1839
1868
|
const prev = pushRoot(root)
|
|
1840
1869
|
const nodes: Node[] = [start]
|
|
1841
1870
|
let handledError = false
|
|
@@ -1873,10 +1902,8 @@ function mountBlock<T>(
|
|
|
1873
1902
|
root,
|
|
1874
1903
|
value: valueSig,
|
|
1875
1904
|
index: indexSig,
|
|
1876
|
-
version: versionSig,
|
|
1877
1905
|
start,
|
|
1878
1906
|
end,
|
|
1879
|
-
valueProxy,
|
|
1880
1907
|
renderCurrent,
|
|
1881
1908
|
}
|
|
1882
1909
|
}
|
|
@@ -1981,7 +2008,7 @@ function patchElement(el: Element, output: FictNode): boolean {
|
|
|
1981
2008
|
if (key === 'class' || key === 'className') {
|
|
1982
2009
|
el.setAttribute('class', value === false || value === null ? '' : String(value))
|
|
1983
2010
|
} else if (key === 'style' && typeof value === 'string') {
|
|
1984
|
-
;(el as
|
|
2011
|
+
;(el as Element & { style: CSSStyleDeclaration }).style.cssText = value
|
|
1985
2012
|
} else if (value === false || value === null || value === undefined) {
|
|
1986
2013
|
el.removeAttribute(key)
|
|
1987
2014
|
} else if (value === true) {
|
|
@@ -2138,8 +2165,4 @@ function removeBlockNodes<T>(block: ManagedBlock<T>): void {
|
|
|
2138
2165
|
}
|
|
2139
2166
|
}
|
|
2140
2167
|
|
|
2141
|
-
function bumpBlockVersion<T>(block: ManagedBlock<T>): void {
|
|
2142
|
-
block.version(block.version() + 1)
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
2168
|
// DOM utility functions are imported from './node-ops' to avoid duplication
|