@srfnstack/fntags 1.2.7 → 1.2.8
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 +13 -2
- package/src/fntags.d.mts.map +1 -1
- package/src/fntags.mjs +48 -3
package/package.json
CHANGED
package/src/fntags.d.mts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toggle dev-mode warnings. Defaults to true. Call `setDevMode(false)` in
|
|
3
|
+
* production to skip diagnostic checks such as the focus-loss warning fired
|
|
4
|
+
* when bindAs replaces a subtree containing the focused element.
|
|
5
|
+
* @param {boolean} on
|
|
6
|
+
*/
|
|
7
|
+
export function setDevMode(on: boolean): void;
|
|
1
8
|
/**
|
|
2
9
|
* A function to create dom elements with the given attributes and children.
|
|
3
10
|
*
|
|
@@ -24,12 +31,14 @@ export function h<T extends HTMLElement | SVGElement>(tag: string, ...children:
|
|
|
24
31
|
* @typedef FnStateObj A container for a state value that can be bound to.
|
|
25
32
|
* @property {(element?: (newValue: T, oldValue: T)=>(Node|any))=>Node} bindAs Bind this state to the given element function. This causes the element to be replaced when the state changes.
|
|
26
33
|
* If called with no parameters, the state's value will be rendered as an element.
|
|
34
|
+
* Do not wrap form inputs (input, textarea, select) in bindAs — it replaces the element on every update and the focused input will lose focus mid-keystroke. Bind the `value` attribute with bindAttr instead.
|
|
27
35
|
* @property {(parent: (()=>(Node|any))|any|Node, element: (childState: FnState)=>(Node|any))=>Node} bindChildren Bind the values of this state to the given element.
|
|
28
36
|
* Values are items/elements of an array.
|
|
29
37
|
* If the current value is not an array, this will behave the same as bindAs.
|
|
30
38
|
* @property {(prop: string)=>Node} bindProp Bind to a property of an object stored in this state instead of the state itself.
|
|
31
39
|
* Shortcut for `mystate.bindAs((current)=> current[prop])`
|
|
32
|
-
* @property {(attribute?: (newValue: T, oldValue: T)=>(string|any))=>any} bindAttr Bind attribute values to state changes
|
|
40
|
+
* @property {(attribute?: (newValue: T, oldValue: T)=>(string|any))=>any} bindAttr Bind attribute values to state changes.
|
|
41
|
+
* Prefer this over bindAs for form inputs: binding the `value` attribute updates the element in place without replacing it, so focus and caret position are preserved.
|
|
33
42
|
* @property {(style?: (newValue: T, oldValue: T)=>string) => string} bindStyle Bind style values to state changes
|
|
34
43
|
* @property {(element?: (selectedKey: any)=>(Node|any))=>Node} bindSelect Bind selected state to an element
|
|
35
44
|
* @property {(attribute?: (selectedKey: any)=>(string|any))=>any} bindSelectAttr Bind selected state to an attribute
|
|
@@ -147,6 +156,7 @@ export type FnStateObj<T> = {
|
|
|
147
156
|
/**
|
|
148
157
|
* Bind this state to the given element function. This causes the element to be replaced when the state changes.
|
|
149
158
|
* If called with no parameters, the state's value will be rendered as an element.
|
|
159
|
+
* Do not wrap form inputs (input, textarea, select) in bindAs — it replaces the element on every update and the focused input will lose focus mid-keystroke. Bind the `value` attribute with bindAttr instead.
|
|
150
160
|
*/
|
|
151
161
|
bindAs: (element?: (newValue: T, oldValue: T) => (Node | any)) => Node;
|
|
152
162
|
/**
|
|
@@ -161,7 +171,8 @@ export type FnStateObj<T> = {
|
|
|
161
171
|
*/
|
|
162
172
|
bindProp: (prop: string) => Node;
|
|
163
173
|
/**
|
|
164
|
-
* Bind attribute values to state changes
|
|
174
|
+
* Bind attribute values to state changes.
|
|
175
|
+
* Prefer this over bindAs for form inputs: binding the `value` attribute updates the element in place without replacing it, so focus and caret position are preserved.
|
|
165
176
|
*/
|
|
166
177
|
bindAttr: (attribute?: (newValue: T, oldValue: T) => (string | any)) => any;
|
|
167
178
|
/**
|
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
|
|
1
|
+
{"version":3,"file":"fntags.d.mts","sourceRoot":"","sources":["fntags.mjs"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,+BAFW,OAAO,QAIjB;AAQD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,2DALW,MAAM,eACH,CAAC,IAAI,MAAO,CAAC,OA6C1B;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,iEAPgB,GAAG,cAqIlB;AA2kBD;;;;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;;;;;;;;GAQG;AACH,wCAJW,MAAM,0BAiBhB;AAED;;;;;;;;;GASG;AACH,oCALW,MAAM,gBACN,GAAG,eACE,GAAG,KAAK,MAAM,OAc7B;AAED;;;;;;;;GAQG;AACH,mCAJW,WAAW,SACX,CAAC,MAAI,IAAI,CAAC,GAAC,IAAI,GACb,WAAW,CAWvB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qDAJkB,IAAI,iBACH,IAAI,CA+DtB;;;;;;;;;;kCAliCmC,CAAC,YAAY,CAAC,KAAG,CAAC,IAAI,GAAC,GAAG,CAAC,KAAG,IAAI;;;;;;2BAG/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;;;;mCAEvC,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
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
// up when the bindAs re-renders. null means we are not inside a render.
|
|
7
7
|
let activeRenderCleanups = null
|
|
8
8
|
|
|
9
|
+
// When on, the framework emits console warnings for misuse that is easy to
|
|
10
|
+
// hit and hard to diagnose (e.g. bindAs replacing a focused input, omitting
|
|
11
|
+
// a key mapper from bindChildren). The checks are cheap but non-zero, so
|
|
12
|
+
// production can opt out via setDevMode(false).
|
|
13
|
+
let devMode = true
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Toggle dev-mode warnings. Defaults to true. Call `setDevMode(false)` in
|
|
17
|
+
* production to skip diagnostic checks such as the focus-loss warning fired
|
|
18
|
+
* when bindAs replaces a subtree containing the focused element.
|
|
19
|
+
* @param {boolean} on
|
|
20
|
+
*/
|
|
21
|
+
export function setDevMode (on) {
|
|
22
|
+
devMode = !!on
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
function runCleanups (elCtx) {
|
|
10
26
|
if (!elCtx.cleanups) return
|
|
11
27
|
for (const unsub of elCtx.cleanups) unsub()
|
|
@@ -88,12 +104,14 @@ function hasNs (val) {
|
|
|
88
104
|
* @typedef FnStateObj A container for a state value that can be bound to.
|
|
89
105
|
* @property {(element?: (newValue: T, oldValue: T)=>(Node|any))=>Node} bindAs Bind this state to the given element function. This causes the element to be replaced when the state changes.
|
|
90
106
|
* If called with no parameters, the state's value will be rendered as an element.
|
|
107
|
+
* Do not wrap form inputs (input, textarea, select) in bindAs — it replaces the element on every update and the focused input will lose focus mid-keystroke. Bind the `value` attribute with bindAttr instead.
|
|
91
108
|
* @property {(parent: (()=>(Node|any))|any|Node, element: (childState: FnState)=>(Node|any))=>Node} bindChildren Bind the values of this state to the given element.
|
|
92
109
|
* Values are items/elements of an array.
|
|
93
110
|
* If the current value is not an array, this will behave the same as bindAs.
|
|
94
111
|
* @property {(prop: string)=>Node} bindProp Bind to a property of an object stored in this state instead of the state itself.
|
|
95
112
|
* Shortcut for `mystate.bindAs((current)=> current[prop])`
|
|
96
|
-
* @property {(attribute?: (newValue: T, oldValue: T)=>(string|any))=>any} bindAttr Bind attribute values to state changes
|
|
113
|
+
* @property {(attribute?: (newValue: T, oldValue: T)=>(string|any))=>any} bindAttr Bind attribute values to state changes.
|
|
114
|
+
* Prefer this over bindAs for form inputs: binding the `value` attribute updates the element in place without replacing it, so focus and caret position are preserved.
|
|
97
115
|
* @property {(style?: (newValue: T, oldValue: T)=>string) => string} bindStyle Bind style values to state changes
|
|
98
116
|
* @property {(element?: (selectedKey: any)=>(Node|any))=>Node} bindSelect Bind selected state to an element
|
|
99
117
|
* @property {(attribute?: (selectedKey: any)=>(string|any))=>any} bindSelectAttr Bind selected state to an attribute
|
|
@@ -428,7 +446,9 @@ function doBindChildren (parent, element) {
|
|
|
428
446
|
throw new Error('You must pass a function to produce child elements.')
|
|
429
447
|
}
|
|
430
448
|
if (typeof ctx.mapKey !== 'function') {
|
|
431
|
-
|
|
449
|
+
if (devMode) {
|
|
450
|
+
console.warn('bindChildren: no mapKey provided — falling back to array index as key, which can misbehave when items are moved, inserted, or removed. Pass a mapKey function as the second argument to fnstate(array, item => item.id).')
|
|
451
|
+
}
|
|
432
452
|
ctx.mapKey = (_, i) => i
|
|
433
453
|
}
|
|
434
454
|
|
|
@@ -459,7 +479,9 @@ function doBindChildren (parent, element) {
|
|
|
459
479
|
|
|
460
480
|
this.subscribe((_, oldState) => {
|
|
461
481
|
if (!Array.isArray(ctx.currentValue)) {
|
|
462
|
-
|
|
482
|
+
if (devMode) {
|
|
483
|
+
console.warn('bindChildren: state was updated to a non-array value. Wrapping it in a single-element array — if you meant to replace the whole binding, use bindAs instead.')
|
|
484
|
+
}
|
|
463
485
|
new Promise((resolve) => {
|
|
464
486
|
this([ctx.currentValue])
|
|
465
487
|
resolve()
|
|
@@ -556,6 +578,28 @@ function propagateReplace (elCtx, oldNode, newNode) {
|
|
|
556
578
|
}
|
|
557
579
|
}
|
|
558
580
|
|
|
581
|
+
// Emit a dev-mode warning when a bindAs replacement would destroy a subtree
|
|
582
|
+
// containing the focused element. This is the most common fntags footgun:
|
|
583
|
+
// wrapping an <input> in bindAs replaces the element on every keystroke, so
|
|
584
|
+
// the user loses focus and caret position mid-edit. The fix is almost always
|
|
585
|
+
// to bind the `value` attribute with bindAttr instead. We only check once per
|
|
586
|
+
// replacement and only touch the DOM when an element is actually focused.
|
|
587
|
+
function warnIfReplacingFocused (oldNode) {
|
|
588
|
+
if (typeof document === 'undefined') return
|
|
589
|
+
const active = document.activeElement
|
|
590
|
+
if (!active || active === document.body) return
|
|
591
|
+
if (oldNode && oldNode.nodeType === 1 && (oldNode === active || oldNode.contains(active))) {
|
|
592
|
+
console.warn(
|
|
593
|
+
'bindAs is about to replace a subtree containing the focused element <' +
|
|
594
|
+
active.tagName.toLowerCase() + '>. The replacement will steal focus and ' +
|
|
595
|
+
'reset caret/selection. For form inputs, bind the `value` attribute with ' +
|
|
596
|
+
'bindAttr instead of wrapping the input in bindAs. ' +
|
|
597
|
+
'Call setDevMode(false) to silence this warning.',
|
|
598
|
+
active
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
559
603
|
const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
|
|
560
604
|
// Clean up subscriptions from the previous render before re-rendering.
|
|
561
605
|
// This prevents nested bindAs/bindAttr/bindStyle subscriptions from
|
|
@@ -595,6 +639,7 @@ const updateReplacer = (ctx, element, elCtx) => (_, oldValue) => {
|
|
|
595
639
|
// is a no-op in the DOM but wastes a microtask.
|
|
596
640
|
if (rendered !== elCtx.current) {
|
|
597
641
|
const oldCurrent = elCtx.current
|
|
642
|
+
if (devMode) warnIfReplacingFocused(oldCurrent)
|
|
598
643
|
elCtx.current.replaceWith(rendered)
|
|
599
644
|
elCtx.current = rendered
|
|
600
645
|
// Keep any ancestor bindAs whose elCtx shared this node in sync with
|