@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fictjs/runtime",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Fict reactive runtime",
5
5
  "publishConfig": {
6
6
  "access": "public",
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 createValueProxy<T>(read: () => T): T {
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: HTMLElement, key: string, value: unknown) => void
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: HTMLElement,
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: HTMLElement, key: string, getValue: () => unknown): Cleanup {
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: HTMLElement, key: string, getValue: () => unknown): Cleanup {
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: HTMLElement,
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(el, next, prev)
441
+ applyStyle(target, next, prev)
441
442
  prev = next
442
443
  })
443
444
  } else {
444
- applyStyle(el, value, undefined)
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: HTMLElement,
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(el, next, prev)
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(el: HTMLElement, value: unknown, prev: unknown): void {
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: HTMLElement,
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: HTMLElement,
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: HTMLElement, key: string, value: boolean): void {
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: HTMLElement, value: unknown, prev: unknown): Record<string, boolean> {
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: HTMLElement,
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: HTMLElement | DocumentFragment,
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
- newNode = createFn ? createFn(value) : document.createTextNode(String(value))
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: HTMLElement | DocumentFragment,
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 HTMLElement/Document type to support event delegation
873
+ // Extend Element/Document type to support event delegation
861
874
  declare global {
862
- interface HTMLElement {
863
- _$host?: HTMLElement
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
- let node = e.target as HTMLElement | null
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
- if (typeof handler === 'function') {
949
- callEventHandler(handler, e, node, hasData ? resolvedNodeData : undefined)
950
- } else if (Array.isArray(handler)) {
951
- const tupleData = resolveData(handler[1])
952
- callEventHandler(handler[0], e, node, tupleData)
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 HTMLElement)._$host &&
962
- node.contains(e.target as Node)
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
- node = path[i] as HTMLElement
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: HTMLElement,
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: HTMLElement,
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: HTMLElement, ref: unknown): Cleanup {
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: HTMLElement) => void)(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: HTMLElement | null }).current = el
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: HTMLElement | null }).current = null
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: HTMLElement | null }).current = null
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: HTMLElement,
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: HTMLElement) => void)(node)
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: HTMLElement,
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: HTMLElement,
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: HTMLElement) => void)(node)
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, index: number) => FictNode,
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(el: HTMLElement, condition: () => boolean, displayValue?: string): void {
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: HTMLElement,
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, index: number) => FictNode,
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 = createSignal<T>(initialValue)
1864
+ const valueSig = createVersionedSignalAccessor<T>(initialValue)
1831
1865
  const indexSig = createSignal<number>(initialIndex)
1832
- const versionSig = createSignal(0)
1833
- const valueProxy = createValueProxy(() => {
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 HTMLElement).style.cssText = value
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