@srfnstack/fntags 1.2.0 → 1.2.2
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 +1 -1
- package/src/fntags.d.mts +0 -3
- package/src/fntags.d.mts.map +1 -1
- package/src/fntags.mjs +101 -12
package/package.json
CHANGED
package/src/fntags.d.mts
CHANGED
package/src/fntags.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,eACH,CAAC,IAAI,MAAO,CAAC,OA6C1B;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,iEAPgB,GAAG,cAoIlB;AA8cD;;;;GAIG;AACH,iCAHW,GAAG,GACD,IAAI,CAuBhB;AAqFD;;;;GAIG;AACH,6BAHW,GAAG,GACD,OAAO,CAInB;AAED;;;;GAIG;AACH,mCAHW,GAAG,GACF,MAAM,CAIjB;AAED;;;;;;;;;;GAUG;AACH,kEALW,MAAM,GAAC,MAAM,OACb,MAAM,YACN,MAAM,EAAE,GAAC,IAAI,EAAE,KAezB;AAOD;;;;;;;;;;;;;;;GAeG;AACH,qDAJkB,IAAI,iBACH,IAAI,CA+DtB;;;;;;;;;kCA/1BmC,CAAC,YAAY,CAAC,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;;2BAE/C,CAAC,MAAI,CAAC,IAAI,GAAC,GAAG,CAAC,CAAC,GAAC,GAAG,GAAC,IAAI,gCAAkC,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;qBAG9E,MAAM,KAAG,IAAI;;;;sCAEI,CAAC,YAAY,CAAC,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;mCACvC,CAAC,YAAY,CAAC,KAAG,MAAM,KAAK,MAAM;;;;yCAC7B,GAAG,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;+CACrB,GAAG,KAAG,CAAC,MAAM,GAAC,GAAG,CAAC,KAAG,GAAG;;;;;;kBAC7C,GAAG,KAAG,IAAI;;;;cAGhB,MAAK,GAAG;;;;;qBACC,CAAC,KAAG,IAAI;;;;;;oBAEV,MAAM,KAAG,GAAG;;;;oBAGZ,MAAM,SAAS,GAAG,mBAAmB,OAAO,KAAG,IAAI;;;;uCAClC,CAAC,YAAY,CAAC,KAAG,IAAI,KAAK,IAAI;;;;eACtD,OAAO;;;;;sDAKqB,CAAC,KAAG,CAAC"}
|
package/src/fntags.mjs
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module fntags
|
|
3
3
|
*/
|
|
4
|
+
|
|
5
|
+
// Tracks subscriptions created during a bindAs render so they can be cleaned
|
|
6
|
+
// up when the bindAs re-renders. null means we are not inside a render.
|
|
7
|
+
let activeRenderCleanups = null
|
|
8
|
+
|
|
9
|
+
function runCleanups (elCtx) {
|
|
10
|
+
if (!elCtx.cleanups) return
|
|
11
|
+
for (const unsub of elCtx.cleanups) unsub()
|
|
12
|
+
elCtx.cleanups = null
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
/**
|
|
5
16
|
* A function to create dom elements with the given attributes and children.
|
|
6
17
|
*
|
|
@@ -247,14 +258,26 @@ function doSubscribe (callback) {
|
|
|
247
258
|
const ctx = this._ctx
|
|
248
259
|
const id = ctx.nextId++
|
|
249
260
|
ctx.observers.push({ id, fn: callback })
|
|
250
|
-
|
|
251
|
-
|
|
261
|
+
// Idempotent unsub — safe to call multiple times
|
|
262
|
+
let active = true
|
|
263
|
+
const unsub = () => {
|
|
264
|
+
if (!active) return
|
|
265
|
+
active = false
|
|
266
|
+
const idx = ctx.observers.findIndex(l => l.id === id)
|
|
267
|
+
if (idx !== -1) ctx.observers.splice(idx, 1)
|
|
252
268
|
}
|
|
269
|
+
// If we're inside a bindAs render, register this subscription for cleanup
|
|
270
|
+
// on the next re-render. This prevents nested bindAs/bindAttr/bindStyle
|
|
271
|
+
// subscriptions from accumulating when the parent bindAs re-renders.
|
|
272
|
+
if (activeRenderCleanups !== null) {
|
|
273
|
+
activeRenderCleanups.push(unsub)
|
|
274
|
+
}
|
|
275
|
+
return unsub
|
|
253
276
|
}
|
|
254
277
|
|
|
255
278
|
const subscribeSelect = (ctx, callback) => {
|
|
256
279
|
const parentCtx = ctx.state.parentCtx
|
|
257
|
-
const key = keyMapper(parentCtx.mapKey, ctx.currentValue)
|
|
280
|
+
const key = keyMapper(parentCtx.mapKey, ctx.currentValue, 0)
|
|
258
281
|
if (parentCtx.selectObservers[key] === undefined) {
|
|
259
282
|
parentCtx.selectObservers[key] = []
|
|
260
283
|
}
|
|
@@ -387,7 +410,7 @@ function doBindChildren (parent, element) {
|
|
|
387
410
|
}
|
|
388
411
|
if (typeof ctx.mapKey !== 'function') {
|
|
389
412
|
console.warn('Using value index as key, may not work correctly when moving items...')
|
|
390
|
-
ctx.mapKey = (
|
|
413
|
+
ctx.mapKey = (_, i) => i
|
|
391
414
|
}
|
|
392
415
|
|
|
393
416
|
if (!Array.isArray(ctx.currentValue)) {
|
|
@@ -420,13 +443,47 @@ const doBind = function (ctx, element, handleReplace) {
|
|
|
420
443
|
if (typeof element !== 'function') {
|
|
421
444
|
throw new Error('You must pass a function to bind with')
|
|
422
445
|
}
|
|
423
|
-
|
|
446
|
+
|
|
447
|
+
// Track subscriptions created during the initial render so they can be
|
|
448
|
+
// cleaned up if this bindAs itself re-renders (updateReplacer) or when
|
|
449
|
+
// a parent bindAs re-renders and tears us down entirely.
|
|
450
|
+
const prevRenderCleanups = activeRenderCleanups
|
|
451
|
+
const thisRenderCleanups = []
|
|
452
|
+
activeRenderCleanups = thisRenderCleanups
|
|
453
|
+
const elCtx = { current: renderNode(evaluateElement(element, ctx.currentValue)), cleanups: thisRenderCleanups }
|
|
454
|
+
// Suppress tracking during handleReplace — the driving subscription is
|
|
455
|
+
// registered separately below as part of a cascading cleanup.
|
|
456
|
+
activeRenderCleanups = null
|
|
424
457
|
handleReplace(elCtx)
|
|
458
|
+
activeRenderCleanups = prevRenderCleanups
|
|
459
|
+
|
|
460
|
+
// If inside a parent bindAs render, register a cascading cleanup that tears
|
|
461
|
+
// down both the driving subscription and all subscriptions created inside
|
|
462
|
+
// this bindAs's render, so they don't outlive the parent's re-render.
|
|
463
|
+
if (prevRenderCleanups !== null && elCtx.drivingUnsub) {
|
|
464
|
+
prevRenderCleanups.push(() => {
|
|
465
|
+
elCtx.drivingUnsub()
|
|
466
|
+
runCleanups(elCtx)
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
425
470
|
return () => elCtx.current
|
|
426
471
|
}
|
|
427
472
|
|
|
428
473
|
const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
|
|
474
|
+
// Clean up subscriptions from the previous render before re-rendering.
|
|
475
|
+
// This prevents nested bindAs/bindAttr/bindStyle subscriptions from
|
|
476
|
+
// accumulating each time the parent state changes.
|
|
477
|
+
runCleanups(elCtx)
|
|
478
|
+
|
|
479
|
+
// Track subscriptions created during this re-render for cleanup next time.
|
|
480
|
+
const prevRenderCleanups = activeRenderCleanups
|
|
481
|
+
const thisRenderCleanups = []
|
|
482
|
+
activeRenderCleanups = thisRenderCleanups
|
|
429
483
|
let rendered = renderNode(evaluateElement(element, ctx.currentValue, oldValue))
|
|
484
|
+
activeRenderCleanups = prevRenderCleanups
|
|
485
|
+
elCtx.cleanups = thisRenderCleanups
|
|
486
|
+
|
|
430
487
|
if (rendered !== undefined) {
|
|
431
488
|
if (elCtx.current.key !== undefined) {
|
|
432
489
|
rendered.key = elCtx.current.key
|
|
@@ -438,8 +495,13 @@ const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
|
|
|
438
495
|
}
|
|
439
496
|
// Perform this action on the next event loop to give the parent a chance to render
|
|
440
497
|
new Promise((resolve) => {
|
|
441
|
-
|
|
442
|
-
|
|
498
|
+
// Guard: skip replaceWith when the same element is returned (e.g. when
|
|
499
|
+
// toggling between two pre-built element references). el.replaceWith(el)
|
|
500
|
+
// is a no-op in the DOM but wastes a microtask.
|
|
501
|
+
if (rendered !== elCtx.current) {
|
|
502
|
+
elCtx.current.replaceWith(rendered)
|
|
503
|
+
elCtx.current = rendered
|
|
504
|
+
}
|
|
443
505
|
rendered = null
|
|
444
506
|
resolve()
|
|
445
507
|
}).catch(e => {
|
|
@@ -461,7 +523,10 @@ function doBindSelect (element) {
|
|
|
461
523
|
function doBindAs (element) {
|
|
462
524
|
const ctx = this._ctx
|
|
463
525
|
const el = element ?? this
|
|
464
|
-
return doBind(ctx, el, (elCtx) =>
|
|
526
|
+
return doBind(ctx, el, (elCtx) => {
|
|
527
|
+
// Store so doBind can build a cascading cleanup when nested inside another bindAs
|
|
528
|
+
elCtx.drivingUnsub = this.subscribe(updateReplacer(ctx, el, elCtx))
|
|
529
|
+
})
|
|
465
530
|
}
|
|
466
531
|
|
|
467
532
|
/**
|
|
@@ -476,18 +541,32 @@ function reconcile (ctx, oldState) {
|
|
|
476
541
|
}
|
|
477
542
|
}
|
|
478
543
|
|
|
479
|
-
function keyMapper (mapKey, value) {
|
|
544
|
+
function keyMapper (mapKey, value, i) {
|
|
480
545
|
if (typeof value !== 'object') {
|
|
481
546
|
return value
|
|
482
547
|
} else if (typeof mapKey !== 'function') {
|
|
483
548
|
return 0
|
|
484
549
|
} else {
|
|
485
|
-
return mapKey(value)
|
|
550
|
+
return mapKey(value, i)
|
|
486
551
|
}
|
|
487
552
|
}
|
|
488
553
|
|
|
554
|
+
function cleanElementSubscriptions (bindContext, key) {
|
|
555
|
+
const cleanups = bindContext.elementCleanups?.[key]
|
|
556
|
+
if (!cleanups) return
|
|
557
|
+
for (const unsub of cleanups) unsub()
|
|
558
|
+
delete bindContext.elementCleanups[key]
|
|
559
|
+
}
|
|
560
|
+
|
|
489
561
|
function arrangeElements (ctx, bindContext, oldState) {
|
|
490
562
|
if (!ctx?.currentValue?.length) {
|
|
563
|
+
// Clean up subscriptions for all existing elements before clearing
|
|
564
|
+
if (bindContext.elementCleanups) {
|
|
565
|
+
for (const key in bindContext.elementCleanups) {
|
|
566
|
+
for (const unsub of bindContext.elementCleanups[key]) unsub()
|
|
567
|
+
}
|
|
568
|
+
bindContext.elementCleanups = {}
|
|
569
|
+
}
|
|
491
570
|
bindContext.parent.textContent = ''
|
|
492
571
|
bindContext.boundElementByKey = {}
|
|
493
572
|
bindContext.elementStates = {}
|
|
@@ -506,7 +585,7 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
506
585
|
for (let i = 0; i < ctx.currentValue.length; i++) {
|
|
507
586
|
const rawValue = ctx.currentValue[i]
|
|
508
587
|
// Use the raw value to get the key
|
|
509
|
-
const key = keyMapper(ctx.mapKey, rawValue)
|
|
588
|
+
const key = keyMapper(ctx.mapKey, rawValue, i)
|
|
510
589
|
|
|
511
590
|
if (keys[key]) {
|
|
512
591
|
if (oldState) ctx.state(oldState)
|
|
@@ -531,7 +610,7 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
531
610
|
if (!Array.isArray(currentArr)) return
|
|
532
611
|
|
|
533
612
|
// find the element dynamically because sorting/filtering might have shifted the index
|
|
534
|
-
const idx = currentArr.findIndex(item => keyMapper(ctx.mapKey, item) === key)
|
|
613
|
+
const idx = currentArr.findIndex((item, i) => keyMapper(ctx.mapKey, item, i) === key)
|
|
535
614
|
|
|
536
615
|
if (idx > -1 && currentArr[idx] !== newItem) {
|
|
537
616
|
currentArr[idx] = newItem
|
|
@@ -558,8 +637,16 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
558
637
|
|
|
559
638
|
if (current === undefined) {
|
|
560
639
|
isNew = true
|
|
640
|
+
// Track subscriptions created during the builder call so they can be
|
|
641
|
+
// cleaned up when this item is removed from the array.
|
|
642
|
+
if (!bindContext.elementCleanups) bindContext.elementCleanups = {}
|
|
643
|
+
const prevCleanups = activeRenderCleanups
|
|
644
|
+
const thisElementCleanups = []
|
|
645
|
+
activeRenderCleanups = thisElementCleanups
|
|
561
646
|
// Pass the valueState (the fnstate wrapper) to the user's element builder
|
|
562
647
|
current = bindContext.boundElementByKey[key] = renderNode(evaluateElement(bindContext.element, valueState))
|
|
648
|
+
activeRenderCleanups = prevCleanups
|
|
649
|
+
bindContext.elementCleanups[key] = thisElementCleanups
|
|
563
650
|
current.key = key
|
|
564
651
|
}
|
|
565
652
|
|
|
@@ -580,6 +667,7 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
580
667
|
// the previous was deleted all together, so we will delete it and replace the element
|
|
581
668
|
if (keys[prev.previousSibling.key] === undefined) {
|
|
582
669
|
const keyToDelete = prev.previousSibling.key
|
|
670
|
+
cleanElementSubscriptions(bindContext, keyToDelete)
|
|
583
671
|
delete bindContext.boundElementByKey[keyToDelete]
|
|
584
672
|
delete bindContext.elementStates[keyToDelete] // Cleanup element state
|
|
585
673
|
|
|
@@ -606,6 +694,7 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
606
694
|
// 3. Catch any strays (Cleanup)
|
|
607
695
|
for (const key in bindContext.boundElementByKey) {
|
|
608
696
|
if (keys[key] === undefined) {
|
|
697
|
+
cleanElementSubscriptions(bindContext, key)
|
|
609
698
|
bindContext.boundElementByKey[key].remove()
|
|
610
699
|
delete bindContext.boundElementByKey[key]
|
|
611
700
|
delete bindContext.elementStates[key] // Cleanup element state
|