@srfnstack/fntags 1.2.2 → 1.2.4
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 +5 -5
- package/src/fntags.d.mts +24 -0
- package/src/fntags.d.mts.map +1 -1
- package/src/fntags.mjs +92 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srfnstack/fntags",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"author": "Robert Kempton <r@snow87.com>",
|
|
5
5
|
"private": false,
|
|
6
6
|
"homepage": "https://github.com/srfnstack/fntags",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"typescript": "^5.3.3"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
|
-
"test": "cp src/*.mjs docs/lib/ && npm run lint && cypress run --spec \"test/**\" --headless -b chrome",
|
|
54
|
-
"cypress": "cypress run --headed --spec test/** -b chrome",
|
|
55
|
-
"lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test",
|
|
56
|
-
"lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test",
|
|
53
|
+
"test": "cp src/*.mjs docs/lib/ && npm run lint && npm test --prefix packages/fntags-testing && cypress run --spec \"test/docs/**\" --headless -b chrome",
|
|
54
|
+
"cypress": "cypress run --headed --spec test/docs/** -b chrome",
|
|
55
|
+
"lint": "standard --env browser src && standard --env browser --env jest --global Prism --global cy test/docs",
|
|
56
|
+
"lint:fix": "standard --env browser --fix src && standard --env browser --env jest --global Prism --global cy --fix test/docs",
|
|
57
57
|
"typedef": "rm -rf src/*.mts* && tsc",
|
|
58
58
|
"docs": "typedoc --plugin typedoc-plugin-markdown --out docs/types --json docs/types.json ./src/*.mjs && node scripts/generateApi.js",
|
|
59
59
|
"generate-api": "node scripts/generateApi.js",
|
package/src/fntags.d.mts
CHANGED
|
@@ -92,6 +92,30 @@ export function getAttrs(children: any): object;
|
|
|
92
92
|
* @return {T} The styled element
|
|
93
93
|
*/
|
|
94
94
|
export function styled<T extends HTMLElement | SVGElement>(style: object | string, tag: string, children: object[] | Node[]): T;
|
|
95
|
+
/**
|
|
96
|
+
* Get or create a state instance from the global HMR registry.
|
|
97
|
+
* During development with HMR, the Vite plugin rewrites fnstate() calls to use this function,
|
|
98
|
+
* ensuring state instances survive module reloads.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} id A stable identifier for this state instance (typically 'filepath:varName')
|
|
101
|
+
* @param {any} initialValue The initial value, used only when creating a new state
|
|
102
|
+
* @param {((v: any) => string)?} [mapKey] Optional key function, used only when creating a new state
|
|
103
|
+
* @returns {FnState} The state instance from the registry
|
|
104
|
+
*/
|
|
105
|
+
export function registeredState(id: string, initialValue: any, mapKey?: (v: any) => string): any;
|
|
106
|
+
/**
|
|
107
|
+
* Create an HMR-aware root mount point. Call this in your entry file to mount your app.
|
|
108
|
+
* The returned `rerender` function can be called from an HMR accept callback to re-render
|
|
109
|
+
* the app with new code while preserving state (via registeredState).
|
|
110
|
+
*
|
|
111
|
+
* @param {HTMLElement} container The DOM element to mount into
|
|
112
|
+
* @param {(()=>Node)|Node} appFn A function that returns the app's root element, or the element itself
|
|
113
|
+
* @returns {{ container: HTMLElement, rerender: () => void }}
|
|
114
|
+
*/
|
|
115
|
+
export function hmrRoot(container: HTMLElement, appFn: (() => Node) | Node): {
|
|
116
|
+
container: HTMLElement;
|
|
117
|
+
rerender: () => void;
|
|
118
|
+
};
|
|
95
119
|
/**
|
|
96
120
|
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
97
121
|
* defined in the template.
|
package/src/fntags.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,cAqIlB;AAqfD;;;;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;;;;;;;;;GASG;AACH,oCALW,MAAM,gBACN,GAAG,eACE,GAAG,KAAK,MAAM,OAc7B;AAED;;;;;;;;GAQG;AACH,mCAJW,WAAW,SACX,CAAC,MAAI,IAAI,CAAC,GAAC,IAAI,GACb;IAAE,SAAS,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAAE,CAU5D;AAED;;;;;;;;;;;;;;;GAeG;AACH,qDAJkB,IAAI,iBACH,IAAI,CA+DtB;;;;;;;;;kCAj7BmC,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
|
@@ -141,7 +141,8 @@ export function fnstate (initialValue, mapKey) {
|
|
|
141
141
|
} else {
|
|
142
142
|
const oldState = ctx.currentValue
|
|
143
143
|
ctx.currentValue = newState
|
|
144
|
-
|
|
144
|
+
const snapshot = ctx.observers.slice()
|
|
145
|
+
for (const observer of snapshot) {
|
|
145
146
|
observer.fn(newState, oldState)
|
|
146
147
|
}
|
|
147
148
|
}
|
|
@@ -282,6 +283,24 @@ const subscribeSelect = (ctx, callback) => {
|
|
|
282
283
|
parentCtx.selectObservers[key] = []
|
|
283
284
|
}
|
|
284
285
|
parentCtx.selectObservers[key].push(callback)
|
|
286
|
+
|
|
287
|
+
// Return an unsub that removes this callback, and register it in
|
|
288
|
+
// activeRenderCleanups so parent bindAs re-renders clean it up.
|
|
289
|
+
let active = true
|
|
290
|
+
const unsub = () => {
|
|
291
|
+
if (!active) return
|
|
292
|
+
active = false
|
|
293
|
+
const arr = parentCtx.selectObservers[key]
|
|
294
|
+
if (arr) {
|
|
295
|
+
const idx = arr.indexOf(callback)
|
|
296
|
+
if (idx !== -1) arr.splice(idx, 1)
|
|
297
|
+
if (arr.length === 0) delete parentCtx.selectObservers[key]
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (activeRenderCleanups !== null) {
|
|
301
|
+
activeRenderCleanups.push(unsub)
|
|
302
|
+
}
|
|
303
|
+
return unsub
|
|
285
304
|
}
|
|
286
305
|
|
|
287
306
|
function doBindSelectAttr (attribute) {
|
|
@@ -335,10 +354,10 @@ function doSelect (key) {
|
|
|
335
354
|
const currentSelected = ctx.selected
|
|
336
355
|
ctx.selected = key
|
|
337
356
|
if (ctx.selectObservers[currentSelected] !== undefined) {
|
|
338
|
-
for (const obs of ctx.selectObservers[currentSelected]) obs(ctx.selected)
|
|
357
|
+
for (const obs of ctx.selectObservers[currentSelected].slice()) obs(ctx.selected)
|
|
339
358
|
}
|
|
340
359
|
if (ctx.selectObservers[ctx.selected] !== undefined) {
|
|
341
|
-
for (const obs of ctx.selectObservers[ctx.selected]) obs(ctx.selected)
|
|
360
|
+
for (const obs of ctx.selectObservers[ctx.selected].slice()) obs(ctx.selected)
|
|
342
361
|
}
|
|
343
362
|
}
|
|
344
363
|
|
|
@@ -417,7 +436,27 @@ function doBindChildren (parent, element) {
|
|
|
417
436
|
throw new Error('You can only use bindChildren with a state that contains an array. try myState([mystate]) before calling this function.')
|
|
418
437
|
}
|
|
419
438
|
|
|
420
|
-
|
|
439
|
+
const bindContext = { element, parent }
|
|
440
|
+
ctx.bindContexts.push(bindContext)
|
|
441
|
+
|
|
442
|
+
// If inside a parent bindAs render, register a cleanup that tears down all
|
|
443
|
+
// per-item subscriptions when the parent re-renders or is itself torn down.
|
|
444
|
+
// Without this, nested bindAttr/bindStyle/bindAs subscriptions created for
|
|
445
|
+
// each array item would be orphaned when the parent replaces this tree.
|
|
446
|
+
if (activeRenderCleanups !== null) {
|
|
447
|
+
activeRenderCleanups.push(() => {
|
|
448
|
+
if (bindContext.elementCleanups) {
|
|
449
|
+
for (const key in bindContext.elementCleanups) {
|
|
450
|
+
for (const unsub of bindContext.elementCleanups[key]) unsub()
|
|
451
|
+
}
|
|
452
|
+
bindContext.elementCleanups = {}
|
|
453
|
+
}
|
|
454
|
+
// Remove this bindContext so reconcile doesn't operate on stale state
|
|
455
|
+
const idx = ctx.bindContexts.indexOf(bindContext)
|
|
456
|
+
if (idx !== -1) ctx.bindContexts.splice(idx, 1)
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
421
460
|
this.subscribe((_, oldState) => {
|
|
422
461
|
if (!Array.isArray(ctx.currentValue)) {
|
|
423
462
|
console.warn('A state used with bindChildren was updated to a non array value. This will be converted to an array of 1 and the state will be updated.')
|
|
@@ -460,9 +499,10 @@ const doBind = function (ctx, element, handleReplace) {
|
|
|
460
499
|
// If inside a parent bindAs render, register a cascading cleanup that tears
|
|
461
500
|
// down both the driving subscription and all subscriptions created inside
|
|
462
501
|
// this bindAs's render, so they don't outlive the parent's re-render.
|
|
463
|
-
if (prevRenderCleanups !== null && elCtx.drivingUnsub) {
|
|
502
|
+
if (prevRenderCleanups !== null && (elCtx.drivingUnsub || elCtx.selectUnsub)) {
|
|
464
503
|
prevRenderCleanups.push(() => {
|
|
465
|
-
elCtx.drivingUnsub()
|
|
504
|
+
if (elCtx.drivingUnsub) elCtx.drivingUnsub()
|
|
505
|
+
if (elCtx.selectUnsub) elCtx.selectUnsub()
|
|
466
506
|
runCleanups(elCtx)
|
|
467
507
|
})
|
|
468
508
|
}
|
|
@@ -517,7 +557,9 @@ const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
|
|
|
517
557
|
function doBindSelect (element) {
|
|
518
558
|
element = element ?? this
|
|
519
559
|
const ctx = this._ctx
|
|
520
|
-
return doBind(ctx, element, (elCtx) =>
|
|
560
|
+
return doBind(ctx, element, (elCtx) => {
|
|
561
|
+
elCtx.selectUnsub = subscribeSelect(ctx, updateReplacer(ctx, element, elCtx))
|
|
562
|
+
})
|
|
521
563
|
}
|
|
522
564
|
|
|
523
565
|
function doBindAs (element) {
|
|
@@ -671,9 +713,7 @@ function arrangeElements (ctx, bindContext, oldState) {
|
|
|
671
713
|
delete bindContext.boundElementByKey[keyToDelete]
|
|
672
714
|
delete bindContext.elementStates[keyToDelete] // Cleanup element state
|
|
673
715
|
|
|
674
|
-
|
|
675
|
-
delete ctx.selectObservers[keyToDelete]
|
|
676
|
-
}
|
|
716
|
+
delete ctx.selectObservers[keyToDelete]
|
|
677
717
|
prev.previousSibling.replaceWith(current)
|
|
678
718
|
} else if (isNew) {
|
|
679
719
|
// insertAdjacentElement is faster, but some nodes don't have it (lookin' at you text)
|
|
@@ -873,6 +913,48 @@ const stringifyStyle = style =>
|
|
|
873
913
|
? style
|
|
874
914
|
: Object.keys(style).map(prop => `${prop}:${style[prop]}`).join(';')
|
|
875
915
|
|
|
916
|
+
/**
|
|
917
|
+
* Get or create a state instance from the global HMR registry.
|
|
918
|
+
* During development with HMR, the Vite plugin rewrites fnstate() calls to use this function,
|
|
919
|
+
* ensuring state instances survive module reloads.
|
|
920
|
+
*
|
|
921
|
+
* @param {string} id A stable identifier for this state instance (typically 'filepath:varName')
|
|
922
|
+
* @param {any} initialValue The initial value, used only when creating a new state
|
|
923
|
+
* @param {((v: any) => string)?} [mapKey] Optional key function, used only when creating a new state
|
|
924
|
+
* @returns {FnState} The state instance from the registry
|
|
925
|
+
*/
|
|
926
|
+
export function registeredState (id, initialValue, mapKey) {
|
|
927
|
+
if (!globalThis.__fntags_registry) {
|
|
928
|
+
globalThis.__fntags_registry = new Map()
|
|
929
|
+
}
|
|
930
|
+
const registry = globalThis.__fntags_registry
|
|
931
|
+
if (registry.has(id)) {
|
|
932
|
+
return registry.get(id)
|
|
933
|
+
}
|
|
934
|
+
const state = fnstate(initialValue, mapKey)
|
|
935
|
+
registry.set(id, state)
|
|
936
|
+
return state
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Create an HMR-aware root mount point. Call this in your entry file to mount your app.
|
|
941
|
+
* The returned `rerender` function can be called from an HMR accept callback to re-render
|
|
942
|
+
* the app with new code while preserving state (via registeredState).
|
|
943
|
+
*
|
|
944
|
+
* @param {HTMLElement} container The DOM element to mount into
|
|
945
|
+
* @param {(()=>Node)|Node} appFn A function that returns the app's root element, or the element itself
|
|
946
|
+
* @returns {{ container: HTMLElement, rerender: () => void }}
|
|
947
|
+
*/
|
|
948
|
+
export function hmrRoot (container, appFn) {
|
|
949
|
+
const render = () => {
|
|
950
|
+
container.innerHTML = ''
|
|
951
|
+
const result = typeof appFn === 'function' ? appFn() : appFn
|
|
952
|
+
container.appendChild(renderNode(result))
|
|
953
|
+
}
|
|
954
|
+
render()
|
|
955
|
+
return { container, rerender: render }
|
|
956
|
+
}
|
|
957
|
+
|
|
876
958
|
/**
|
|
877
959
|
* Create a compiled template function. The returned function takes a single object that contains the properties
|
|
878
960
|
* defined in the template.
|