@fictjs/runtime 0.0.7 → 0.0.9

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/src/dom.ts CHANGED
@@ -25,7 +25,14 @@ import {
25
25
  type AttributeSetter,
26
26
  type BindingHandle,
27
27
  } from './binding'
28
- import { Properties, ChildProperties, Aliases, getPropAlias, SVGNamespace } from './constants'
28
+ import {
29
+ Properties,
30
+ ChildProperties,
31
+ Aliases,
32
+ getPropAlias,
33
+ SVGElements,
34
+ SVGNamespace,
35
+ } from './constants'
29
36
  import { __fictPushContext, __fictPopContext } from './hooks'
30
37
  import { Fragment } from './jsx'
31
38
  import {
@@ -43,6 +50,11 @@ import { createPropsProxy, unwrapProps } from './props'
43
50
  import { untrack } from './scheduler'
44
51
  import type { DOMElement, FictNode, FictVNode, RefObject } from './types'
45
52
 
53
+ type NamespaceContext = 'svg' | 'mathml' | null
54
+
55
+ const SVG_NS = 'http://www.w3.org/2000/svg'
56
+ const MATHML_NS = 'http://www.w3.org/1998/Math/MathML'
57
+
46
58
  // ============================================================================
47
59
  // Main Render Function
48
60
  // ============================================================================
@@ -104,6 +116,19 @@ export function render(view: () => FictNode, container: HTMLElement): () => void
104
116
  * - Reactive values (functions returning any of the above)
105
117
  */
106
118
  export function createElement(node: FictNode): DOMElement {
119
+ return createElementWithContext(node, null)
120
+ }
121
+
122
+ function resolveNamespace(tagName: string, namespace: NamespaceContext): NamespaceContext {
123
+ if (tagName === 'svg') return 'svg'
124
+ if (tagName === 'math') return 'mathml'
125
+ if (namespace === 'mathml') return 'mathml'
126
+ if (namespace === 'svg') return 'svg'
127
+ if (SVGElements.has(tagName)) return 'svg'
128
+ return null
129
+ }
130
+
131
+ function createElementWithContext(node: FictNode, namespace: NamespaceContext): DOMElement {
107
132
  // Already a DOM node - pass through
108
133
  if (node instanceof Node) {
109
134
  return node
@@ -118,7 +143,12 @@ export function createElement(node: FictNode): DOMElement {
118
143
  if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
119
144
  // Handle BindingHandle (createList, createConditional, etc)
120
145
  if ('marker' in node) {
121
- return createElement((node as { marker: unknown }).marker as FictNode)
146
+ const handle = node as { marker: unknown; dispose?: () => void }
147
+ // Register dispose cleanup if available
148
+ if (typeof handle.dispose === 'function') {
149
+ registerRootCleanup(handle.dispose)
150
+ }
151
+ return createElement(handle.marker as FictNode)
122
152
  }
123
153
 
124
154
  const nodeRecord = node as unknown as Record<PropertyKey, unknown>
@@ -134,7 +164,7 @@ export function createElement(node: FictNode): DOMElement {
134
164
  if (Array.isArray(node)) {
135
165
  const frag = document.createDocumentFragment()
136
166
  for (const child of node) {
137
- appendChildNode(frag, child)
167
+ appendChildNode(frag, child, namespace)
138
168
  }
139
169
  return frag
140
170
  }
@@ -186,7 +216,7 @@ export function createElement(node: FictNode): DOMElement {
186
216
  __fictPushContext()
187
217
  const rendered = vnode.type(props)
188
218
  __fictPopContext()
189
- return createElement(rendered as FictNode)
219
+ return createElementWithContext(rendered as FictNode, namespace)
190
220
  } catch (err) {
191
221
  __fictPopContext()
192
222
  if (handleSuspend(err as any)) {
@@ -201,15 +231,26 @@ export function createElement(node: FictNode): DOMElement {
201
231
  if (vnode.type === Fragment) {
202
232
  const frag = document.createDocumentFragment()
203
233
  const children = vnode.props?.children as FictNode | FictNode[] | undefined
204
- appendChildren(frag, children)
234
+ appendChildren(frag, children, namespace)
205
235
  return frag
206
236
  }
207
237
 
208
238
  // HTML Element
209
239
  const tagName = typeof vnode.type === 'string' ? vnode.type : 'div'
210
- const el = document.createElement(tagName)
211
- applyProps(el, vnode.props ?? {})
212
- return el
240
+ const resolvedNamespace = resolveNamespace(tagName, namespace)
241
+ const el =
242
+ resolvedNamespace === 'svg'
243
+ ? document.createElementNS(SVG_NS, tagName)
244
+ : resolvedNamespace === 'mathml'
245
+ ? document.createElementNS(MATHML_NS, tagName)
246
+ : document.createElement(tagName)
247
+ applyProps(el, vnode.props ?? {}, resolvedNamespace === 'svg')
248
+ appendChildren(
249
+ el as unknown as ParentNode & Node,
250
+ vnode.props?.children as FictNode | FictNode[] | undefined,
251
+ tagName === 'foreignObject' ? null : resolvedNamespace,
252
+ )
253
+ return el as DOMElement
213
254
  }
214
255
 
215
256
  /**
@@ -231,7 +272,7 @@ export function template(
231
272
 
232
273
  const create = (): Node => {
233
274
  const t = isMathML
234
- ? document.createElementNS('http://www.w3.org/1998/Math/MathML', 'template')
275
+ ? document.createElementNS(MATHML_NS, 'template')
235
276
  : document.createElement('template')
236
277
  t.innerHTML = html
237
278
 
@@ -279,7 +320,11 @@ function isBindingHandle(node: unknown): node is BindingHandle {
279
320
  /**
280
321
  * Append a child node to a parent, handling all node types including reactive values.
281
322
  */
282
- function appendChildNode(parent: HTMLElement | DocumentFragment, child: FictNode): void {
323
+ function appendChildNode(
324
+ parent: ParentNode & Node,
325
+ child: FictNode,
326
+ namespace: NamespaceContext,
327
+ ): void {
283
328
  // Skip nullish values
284
329
  if (child === null || child === undefined || child === false) {
285
330
  return
@@ -287,7 +332,7 @@ function appendChildNode(parent: HTMLElement | DocumentFragment, child: FictNode
287
332
 
288
333
  // Handle BindingHandle (recursive)
289
334
  if (isBindingHandle(child)) {
290
- appendChildNode(parent, child.marker)
335
+ appendChildNode(parent, child.marker, namespace)
291
336
  // Flush pending nodes now that markers are in the DOM
292
337
  child.flush?.()
293
338
  return
@@ -296,14 +341,14 @@ function appendChildNode(parent: HTMLElement | DocumentFragment, child: FictNode
296
341
  // Handle getter function (recursive)
297
342
  if (typeof child === 'function' && (child as () => FictNode).length === 0) {
298
343
  const childGetter = child as () => FictNode
299
- createChildBinding(parent as HTMLElement | DocumentFragment, childGetter, createElement)
344
+ createChildBinding(parent, childGetter, node => createElementWithContext(node, namespace))
300
345
  return
301
346
  }
302
347
 
303
348
  // Static child - create element and append
304
349
  if (Array.isArray(child)) {
305
350
  for (const item of child) {
306
- appendChildNode(parent, item)
351
+ appendChildNode(parent, item, namespace)
307
352
  }
308
353
  return
309
354
  }
@@ -313,14 +358,14 @@ function appendChildNode(parent: HTMLElement | DocumentFragment, child: FictNode
313
358
  if (typeof child !== 'object' || child === null) {
314
359
  domNode = document.createTextNode(String(child ?? ''))
315
360
  } else {
316
- domNode = createElement(child as any) as Node
361
+ domNode = createElementWithContext(child as any, namespace) as Node
317
362
  }
318
363
 
319
364
  // Handle DocumentFragment manually to avoid JSDOM issues
320
365
  if (domNode.nodeType === 11) {
321
366
  const children = Array.from(domNode.childNodes)
322
367
  for (const node of children) {
323
- appendChildNode(parent, node)
368
+ appendChildNode(parent, node as FictNode, namespace)
324
369
  }
325
370
  return
326
371
  }
@@ -345,19 +390,20 @@ function appendChildNode(parent: HTMLElement | DocumentFragment, child: FictNode
345
390
  * Append multiple children, handling arrays and nested structures.
346
391
  */
347
392
  function appendChildren(
348
- parent: HTMLElement | DocumentFragment,
393
+ parent: ParentNode & Node,
349
394
  children: FictNode | FictNode[] | undefined,
395
+ namespace: NamespaceContext,
350
396
  ): void {
351
397
  if (children === undefined) return
352
398
 
353
399
  if (Array.isArray(children)) {
354
400
  for (const child of children) {
355
- appendChildren(parent, child)
401
+ appendChildren(parent, child, namespace)
356
402
  }
357
403
  return
358
404
  }
359
405
 
360
- appendChildNode(parent, children)
406
+ appendChildNode(parent, children, namespace)
361
407
  }
362
408
 
363
409
  // ============================================================================
@@ -368,10 +414,10 @@ function appendChildren(
368
414
  * Apply a ref to an element, supporting both callback and object refs.
369
415
  * Both types are automatically cleaned up on unmount.
370
416
  */
371
- function applyRef(el: HTMLElement, value: unknown): void {
417
+ function applyRef(el: Element, value: unknown): void {
372
418
  if (typeof value === 'function') {
373
419
  // Callback ref
374
- const refFn = value as (el: HTMLElement | null) => void
420
+ const refFn = value as (el: Element | null) => void
375
421
  refFn(el)
376
422
 
377
423
  // Match React behavior: call ref(null) on unmount
@@ -382,7 +428,7 @@ function applyRef(el: HTMLElement, value: unknown): void {
382
428
  }
383
429
  } else if (value && typeof value === 'object' && 'current' in value) {
384
430
  // Object ref
385
- const refObj = value as RefObject<HTMLElement>
431
+ const refObj = value as RefObject<Element>
386
432
  refObj.current = el
387
433
 
388
434
  // Auto-cleanup on unmount
@@ -402,7 +448,7 @@ function applyRef(el: HTMLElement, value: unknown): void {
402
448
  * Apply props to an HTML element, setting up reactive bindings as needed.
403
449
  * Uses comprehensive property constants for correct attribute/property handling.
404
450
  */
405
- function applyProps(el: HTMLElement, props: Record<string, unknown>, isSVG = false): void {
451
+ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false): void {
406
452
  props = unwrapProps(props)
407
453
  const tagName = el.tagName
408
454
 
@@ -545,10 +591,6 @@ function applyProps(el: HTMLElement, props: Record<string, unknown>, isSVG = fal
545
591
  const attrName = Aliases[key] || key
546
592
  createAttributeBinding(el, attrName, value as MaybeReactive<unknown>, setAttribute)
547
593
  }
548
-
549
- // Handle children
550
- const children = props.children as FictNode | FictNode[] | undefined
551
- appendChildren(el, children)
552
594
  }
553
595
 
554
596
  /**
@@ -565,7 +607,7 @@ function toPropertyName(name: string): string {
565
607
  /**
566
608
  * Set an attribute on an element, handling various value types.
567
609
  */
568
- const setAttribute: AttributeSetter = (el: HTMLElement, key: string, value: unknown): void => {
610
+ const setAttribute: AttributeSetter = (el: Element, key: string, value: unknown): void => {
569
611
  // Remove attribute for nullish/false values
570
612
  if (value === undefined || value === null || value === false) {
571
613
  el.removeAttribute(key)
@@ -598,7 +640,7 @@ const setAttribute: AttributeSetter = (el: HTMLElement, key: string, value: unkn
598
640
  /**
599
641
  * Set a property on an element, ensuring nullish values clear sensibly.
600
642
  */
601
- const setProperty: AttributeSetter = (el: HTMLElement, key: string, value: unknown): void => {
643
+ const setProperty: AttributeSetter = (el: Element, key: string, value: unknown): void => {
602
644
  if (value === undefined || value === null) {
603
645
  const fallback = key === 'checked' || key === 'selected' ? false : ''
604
646
  ;(el as unknown as Record<string, unknown>)[key] = fallback
@@ -610,7 +652,7 @@ const setProperty: AttributeSetter = (el: HTMLElement, key: string, value: unkno
610
652
  for (const k in value as Record<string, string>) {
611
653
  const v = (value as Record<string, string>)[k]
612
654
  if (v !== undefined) {
613
- ;(el.style as unknown as Record<string, string>)[k] = String(v)
655
+ ;((el as HTMLElement).style as unknown as Record<string, string>)[k] = String(v)
614
656
  }
615
657
  }
616
658
  return
@@ -622,14 +664,14 @@ const setProperty: AttributeSetter = (el: HTMLElement, key: string, value: unkno
622
664
  /**
623
665
  * Set innerHTML on an element (used for dangerouslySetInnerHTML)
624
666
  */
625
- const setInnerHTML: AttributeSetter = (el: HTMLElement, _key: string, value: unknown): void => {
626
- el.innerHTML = value == null ? '' : String(value)
667
+ const setInnerHTML: AttributeSetter = (el: Element, _key: string, value: unknown): void => {
668
+ ;(el as HTMLElement).innerHTML = value == null ? '' : String(value)
627
669
  }
628
670
 
629
671
  /**
630
672
  * Set a boolean attribute on an element (empty string when true, removed when false)
631
673
  */
632
- const setBoolAttribute: AttributeSetter = (el: HTMLElement, key: string, value: unknown): void => {
674
+ const setBoolAttribute: AttributeSetter = (el: Element, key: string, value: unknown): void => {
633
675
  if (value) {
634
676
  el.setAttribute(key, '')
635
677
  } else {
@@ -640,7 +682,7 @@ const setBoolAttribute: AttributeSetter = (el: HTMLElement, key: string, value:
640
682
  /**
641
683
  * Set an attribute with a namespace (for SVG xlink:href, etc.)
642
684
  */
643
- function setAttributeNS(el: HTMLElement, namespace: string, name: string, value: unknown): void {
685
+ function setAttributeNS(el: Element, namespace: string, name: string, value: unknown): void {
644
686
  if (value == null) {
645
687
  el.removeAttributeNS(namespace, name)
646
688
  } else {
@@ -4,6 +4,7 @@ import {
4
4
  createRootContext,
5
5
  destroyRoot,
6
6
  flushOnMount,
7
+ getCurrentRoot,
7
8
  pushRoot,
8
9
  popRoot,
9
10
  registerErrorHandler,
@@ -24,6 +25,7 @@ export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
24
25
  fragment.appendChild(marker)
25
26
 
26
27
  const currentView = createSignal<FictNode | null>(props.children ?? null)
28
+ const hostRoot = getCurrentRoot()
27
29
 
28
30
  let cleanup: (() => void) | undefined
29
31
  let activeNodes: Node[] = []
@@ -52,7 +54,7 @@ export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
52
54
  return
53
55
  }
54
56
 
55
- const root = createRootContext()
57
+ const root = createRootContext(hostRoot)
56
58
  const prev = pushRoot(root)
57
59
  let nodes: Node[] = []
58
60
  try {
@@ -11,6 +11,7 @@ import {
11
11
  createRootContext,
12
12
  destroyRoot,
13
13
  flushOnMount,
14
+ getCurrentRoot,
14
15
  popRoot,
15
16
  pushRoot,
16
17
  type RootContext,
@@ -200,7 +201,7 @@ function removeBlockRange(block: MarkerBlock): void {
200
201
  }
201
202
  }
202
203
 
203
- function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
204
+ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
204
205
  let current = initialValue
205
206
  let version = 0
206
207
  const track = createSignal(version)
@@ -243,6 +244,16 @@ export function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
243
244
  container.nextBlocks.clear()
244
245
 
245
246
  // Remove nodes (including markers)
247
+ // Check if markers are still in DOM before using Range
248
+ if (!startMarker.parentNode || !endMarker.parentNode) {
249
+ // Markers already removed, nothing to do
250
+ container.currentNodes = []
251
+ container.nextNodes = []
252
+ container.orderedBlocks.length = 0
253
+ container.nextOrderedBlocks.length = 0
254
+ container.orderedIndexByKey.clear()
255
+ return
256
+ }
246
257
  const range = document.createRange()
247
258
  range.setStartBefore(startMarker)
248
259
  range.setEndAfter(endMarker)
@@ -292,6 +303,7 @@ export function createKeyedBlock<T>(
292
303
  index: number,
293
304
  render: (item: Signal<T>, index: Signal<number>, key: string | number) => Node[],
294
305
  needsIndex = true,
306
+ hostRoot?: RootContext,
295
307
  ): KeyedBlock<T> {
296
308
  // Use versioned signal for all item types; avoid diffing proxy overhead for objects
297
309
  const itemSig = createVersionedSignalAccessor(item)
@@ -303,7 +315,7 @@ export function createKeyedBlock<T>(
303
315
  index = next as number
304
316
  return index
305
317
  }) as Signal<number>)
306
- const root = createRootContext()
318
+ const root = createRootContext(hostRoot)
307
319
  const prevRoot = pushRoot(root)
308
320
 
309
321
  // Isolate child effects from the outer effect (e.g., performDiff) by clearing activeSub.
@@ -399,6 +411,7 @@ function createFineGrainedKeyedList<T>(
399
411
  needsIndex: boolean,
400
412
  ): KeyedListBinding {
401
413
  const container = createKeyedListContainer<T>()
414
+ const hostRoot = getCurrentRoot()
402
415
  const fragment = document.createDocumentFragment()
403
416
  fragment.append(container.startMarker, container.endMarker)
404
417
  let pendingItems: T[] | null = null
@@ -491,7 +504,7 @@ function createFineGrainedKeyedList<T>(
491
504
  }
492
505
 
493
506
  // Create new block
494
- block = createKeyedBlock(key, item, index, renderItem, needsIndex)
507
+ block = createKeyedBlock(key, item, index, renderItem, needsIndex, hostRoot)
495
508
  }
496
509
 
497
510
  const resolvedBlock = block!
package/src/ref.ts CHANGED
@@ -20,6 +20,6 @@ import type { RefObject } from './types'
20
20
  * }
21
21
  * ```
22
22
  */
23
- export function createRef<T extends HTMLElement = HTMLElement>(): RefObject<T> {
23
+ export function createRef<T extends Element = HTMLElement>(): RefObject<T> {
24
24
  return { current: null }
25
25
  }
package/src/store.ts CHANGED
@@ -2,6 +2,7 @@ import { signal, batch, type SignalAccessor } from './signal'
2
2
 
3
3
  const PROXY = Symbol('fict:store-proxy')
4
4
  const TARGET = Symbol('fict:store-target')
5
+ const ITERATE_KEY = Symbol('fict:iterate')
5
6
 
6
7
  // ============================================================================
7
8
  // Store (Deep Proxy)
@@ -57,22 +58,43 @@ function wrap<T>(value: T): T {
57
58
  // Recursively wrap objects
58
59
  return wrap(value)
59
60
  },
61
+ has(target, prop) {
62
+ const result = Reflect.has(target, prop)
63
+ track(target, prop)
64
+ return result
65
+ },
66
+ ownKeys(target) {
67
+ track(target, ITERATE_KEY)
68
+ return Reflect.ownKeys(target)
69
+ },
70
+ getOwnPropertyDescriptor(target, prop) {
71
+ track(target, prop)
72
+ return Reflect.getOwnPropertyDescriptor(target, prop)
73
+ },
60
74
  set(target, prop, value, receiver) {
61
75
  if (prop === PROXY || prop === TARGET) return false
62
76
 
77
+ const hadKey = Object.prototype.hasOwnProperty.call(target, prop)
63
78
  const oldValue = Reflect.get(target, prop, receiver)
64
79
  if (oldValue === value) return true
65
80
 
66
81
  const result = Reflect.set(target, prop, value, receiver)
67
82
  if (result) {
68
83
  trigger(target, prop)
84
+ if (!hadKey) {
85
+ trigger(target, ITERATE_KEY)
86
+ }
69
87
  }
70
88
  return result
71
89
  },
72
90
  deleteProperty(target, prop) {
91
+ const hadKey = Object.prototype.hasOwnProperty.call(target, prop)
73
92
  const result = Reflect.deleteProperty(target, prop)
74
93
  if (result) {
75
94
  trigger(target, prop)
95
+ if (hadKey) {
96
+ trigger(target, ITERATE_KEY)
97
+ }
76
98
  }
77
99
  return result
78
100
  },
@@ -99,7 +121,9 @@ function track(target: object, prop: string | symbol) {
99
121
 
100
122
  let s = signals.get(prop)
101
123
  if (!s) {
102
- s = signal(getLastValue(target, prop))
124
+ const initial =
125
+ prop === ITERATE_KEY ? (Reflect.ownKeys(target).length as number) : getLastValue(target, prop)
126
+ s = signal(initial)
103
127
  signals.set(prop, s)
104
128
  }
105
129
  s() // subscribe
@@ -110,7 +134,11 @@ function trigger(target: object, prop: string | symbol) {
110
134
  if (signals) {
111
135
  const s = signals.get(prop)
112
136
  if (s) {
113
- s(getLastValue(target, prop)) // notify with new value
137
+ if (prop === ITERATE_KEY) {
138
+ s(Reflect.ownKeys(target).length)
139
+ } else {
140
+ s(getLastValue(target, prop)) // notify with new value
141
+ }
114
142
  }
115
143
  }
116
144
  }
package/src/types.ts CHANGED
@@ -128,15 +128,15 @@ export interface DOMEventHandlers {
128
128
  // ============================================================================
129
129
 
130
130
  /** Ref callback type */
131
- export type RefCallback<T extends HTMLElement = HTMLElement> = (element: T) => void
131
+ export type RefCallback<T extends Element = HTMLElement> = (element: T) => void
132
132
 
133
133
  /** Ref object type (for future use with createRef) */
134
- export interface RefObject<T extends HTMLElement = HTMLElement> {
134
+ export interface RefObject<T extends Element = HTMLElement> {
135
135
  current: T | null
136
136
  }
137
137
 
138
138
  /** Ref type that can be either callback or object */
139
- export type Ref<T extends HTMLElement = HTMLElement> = RefCallback<T> | RefObject<T>
139
+ export type Ref<T extends Element = HTMLElement> = RefCallback<T> | RefObject<T>
140
140
 
141
141
  // ============================================================================
142
142
  // Style Types