sprae 12.4.17 → 13.0.1
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/core.js +17 -13
- package/directive/_.js +2 -2
- package/directive/change.js +40 -0
- package/directive/class.js +1 -1
- package/directive/intersect.js +44 -0
- package/directive/mount.js +37 -0
- package/directive/ref.js +19 -12
- package/directive/resize.js +39 -0
- package/directive/sequence.js +2 -2
- package/directive/spread.js +2 -2
- package/directive/style.js +1 -1
- package/directive/text.js +22 -1
- package/directive/value.js +11 -37
- package/dist/sprae-csp.js +11 -11
- package/dist/sprae-csp.js.map +4 -4
- package/dist/sprae-csp.umd.js +11 -11
- package/dist/sprae-csp.umd.js.map +4 -4
- package/dist/sprae-preact.js +4 -4
- package/dist/sprae-preact.js.map +4 -4
- package/dist/sprae-preact.umd.js +3 -3
- package/dist/sprae-preact.umd.js.map +4 -4
- package/dist/sprae.js +3 -3
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.umd.js +3 -3
- package/dist/sprae.umd.js.map +4 -4
- package/package.json +1 -1
- package/readme.md +7 -2
- package/sprae.js +22 -5
- package/types/core.d.ts +4 -3
- package/types/core.d.ts.map +1 -1
- package/types/directive/_.d.ts.map +1 -1
- package/types/directive/change.d.ts +5 -0
- package/types/directive/change.d.ts.map +1 -0
- package/types/directive/intersect.d.ts +22 -0
- package/types/directive/intersect.d.ts.map +1 -0
- package/types/directive/mount.d.ts +5 -0
- package/types/directive/mount.d.ts.map +1 -0
- package/types/directive/ref.d.ts +1 -3
- package/types/directive/ref.d.ts.map +1 -1
- package/types/directive/resize.d.ts +21 -0
- package/types/directive/resize.d.ts.map +1 -1
- package/types/directive/spread.d.ts.map +1 -1
- package/types/directive/text.d.ts.map +1 -1
- package/types/directive/value.d.ts +1 -2
- package/types/directive/value.d.ts.map +1 -1
- package/types/sprae.d.ts.map +1 -1
package/core.js
CHANGED
|
@@ -20,6 +20,9 @@ export const _add = Symbol('init')
|
|
|
20
20
|
/** Directive prefix (default: ':') */
|
|
21
21
|
export let prefix = ':';
|
|
22
22
|
|
|
23
|
+
/** Check if element is a custom element (has hyphen in tag name) */
|
|
24
|
+
export const isCE = (el) => el.localName?.includes('-')
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* A reactive signal containing a value.
|
|
25
28
|
* @template T
|
|
@@ -144,16 +147,16 @@ const err = (e, expr, el = currentEl) => {
|
|
|
144
147
|
* @param {Object} [state] - Initial state values to populate the element's reactive state.
|
|
145
148
|
* @returns {SpraeState & Object} The reactive state object associated with the element.
|
|
146
149
|
*/
|
|
147
|
-
const sprae = (
|
|
150
|
+
const sprae = (root = document.body, state) => {
|
|
148
151
|
// repeated call can be caused by eg. :each with new objects with old keys
|
|
149
|
-
if (
|
|
152
|
+
if (root[_state]) return Object.assign(root[_state], state)
|
|
150
153
|
|
|
151
|
-
// console.group('sprae',
|
|
154
|
+
// console.group('sprae', root)
|
|
152
155
|
|
|
153
156
|
// take over existing state instead of creating a clone
|
|
154
157
|
state = store(state || {})
|
|
155
158
|
|
|
156
|
-
let fx = [], offs = []
|
|
159
|
+
let el = root, fx = [], offs = []
|
|
157
160
|
|
|
158
161
|
// on/off all effects
|
|
159
162
|
// we don't call prevOn as convention: everything defined before :else :if won't be disabled by :if
|
|
@@ -181,10 +184,14 @@ const sprae = (el = document.body, state) => {
|
|
|
181
184
|
fx.push(start = dir(el, name.slice(prefix.length), value, state)), offs.push(start())
|
|
182
185
|
|
|
183
186
|
// stop after subsprae like :each, :if, :scope etc.
|
|
184
|
-
|
|
187
|
+
// custom elements: continue processing all directives (prop setters), descent blocked separately (line 189)
|
|
188
|
+
if (_state in el && !isCE(el)) return
|
|
185
189
|
} else i++
|
|
186
190
|
}
|
|
187
191
|
|
|
192
|
+
// custom elements own their children — don't descend
|
|
193
|
+
if (el !== root && isCE(el)) return
|
|
194
|
+
|
|
188
195
|
// :if and :each replace element with text node, which tweaks .children length, but .childNodes length persists
|
|
189
196
|
// real DOM: firstChild/nextSibling avoids array copy; frag.childNodes is already snapshot array
|
|
190
197
|
if (el.firstChild !== undefined) {
|
|
@@ -398,8 +405,10 @@ export const dashcase = (str) => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,
|
|
|
398
405
|
* @param {string | boolean | null | undefined} v - Attribute value (null/false removes, true sets empty)
|
|
399
406
|
* @returns {void}
|
|
400
407
|
*/
|
|
408
|
+
const camelcase = (str) => str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
409
|
+
|
|
401
410
|
export const attr = (el, name, v) => (v == null || v === false) ? el.removeAttribute(name) :
|
|
402
|
-
(
|
|
411
|
+
isCE(el) ? (el[camelcase(name)] = v) :
|
|
403
412
|
el.setAttribute(name, v === true ? "" : v);
|
|
404
413
|
|
|
405
414
|
/**
|
|
@@ -407,9 +416,9 @@ export const attr = (el, name, v) => (v == null || v === false) ? el.removeAttri
|
|
|
407
416
|
* @param {string | string[] | Record<string, boolean> | null | undefined} c - Class input
|
|
408
417
|
* @returns {string} Space-separated class string
|
|
409
418
|
*/
|
|
410
|
-
export const clsx = (c
|
|
419
|
+
export const clsx = (c) => !c ? '' : typeof c === 'string' ? c : (
|
|
411
420
|
Array.isArray(c) ? c.map(clsx) :
|
|
412
|
-
Object.entries(c).reduce((s, [k, v]) =>
|
|
421
|
+
Object.entries(c).reduce((s, [k, v]) => (v && s.push(k), s), [])
|
|
413
422
|
).join(' ')
|
|
414
423
|
|
|
415
424
|
/**
|
|
@@ -448,11 +457,6 @@ export const debounce = (fn, ms, immediate) => {
|
|
|
448
457
|
: ((_count = 0) => (arg, _c = ++_count) => schedule(() => _c == _count && fn(arg)))()
|
|
449
458
|
}
|
|
450
459
|
|
|
451
|
-
/**
|
|
452
|
-
* Parses time string to milliseconds. Supports: 100, 100ms, 1s, 1m
|
|
453
|
-
* @param {string|number} t - Time value
|
|
454
|
-
* @returns {number} Milliseconds
|
|
455
|
-
*/
|
|
456
460
|
export * from './store.js';
|
|
457
461
|
|
|
458
462
|
export default sprae
|
package/directive/_.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attr } from "../core.js";
|
|
1
|
+
import { attr, isCE } from "../core.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Default attribute directive - sets any attribute value.
|
|
@@ -8,4 +8,4 @@ import { attr } from "../core.js";
|
|
|
8
8
|
* @param {string} name - Attribute name
|
|
9
9
|
* @returns {(v: any) => void} Update function
|
|
10
10
|
*/
|
|
11
|
-
export default (el, st, ex, name) => v => attr(el, name, typeof v === 'function' ? v(el.getAttribute(name)) : v)
|
|
11
|
+
export default (el, st, ex, name) => v => attr(el, name, typeof v === 'function' && !isCE(el) ? v(el.getAttribute(name)) : v)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parse, decorate, _dispose } from "../core.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change directive - normalized input write-back observer.
|
|
5
|
+
* Handles type detection, coercion, caret preservation.
|
|
6
|
+
*
|
|
7
|
+
* :change="v => x = v"
|
|
8
|
+
* :change.debounce-300="v => query = v"
|
|
9
|
+
*
|
|
10
|
+
* @param {Element} el - Form element
|
|
11
|
+
* @param {Object} state - State object
|
|
12
|
+
* @param {string} expr - Handler expression (receives coerced value)
|
|
13
|
+
* @param {string} name - Directive name with modifiers
|
|
14
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
15
|
+
*/
|
|
16
|
+
export default (el, state, expr, name) => {
|
|
17
|
+
const [, ...mods] = name.split('.'),
|
|
18
|
+
evaluate = parse(expr).bind(el)
|
|
19
|
+
|
|
20
|
+
// coerce value from element based on input type
|
|
21
|
+
const coerce =
|
|
22
|
+
el.type === 'checkbox' ? () => el.checked :
|
|
23
|
+
el.type === 'select-multiple' ? () => [...el.selectedOptions].map(o => o.value) :
|
|
24
|
+
/^(date|time|month|week)/.test(el.type) ? () => el.value :
|
|
25
|
+
() => el.selectedIndex < 0 ? null : isNaN(el.valueAsNumber) ? el.value : el.valueAsNumber
|
|
26
|
+
|
|
27
|
+
const handler = decorate(Object.assign(() => {
|
|
28
|
+
evaluate(state, fn => typeof fn === 'function' ? fn(coerce()) : fn)
|
|
29
|
+
}, { target: el }), mods)
|
|
30
|
+
|
|
31
|
+
el.addEventListener('input', handler)
|
|
32
|
+
el.addEventListener('change', handler)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
[_dispose]() {
|
|
36
|
+
el.removeEventListener('input', handler)
|
|
37
|
+
el.removeEventListener('change', handler)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/directive/class.js
CHANGED
|
@@ -14,7 +14,7 @@ export default (el, st, ex, name) => {
|
|
|
14
14
|
|
|
15
15
|
return (v) => {
|
|
16
16
|
_new = new Set
|
|
17
|
-
if (v) clsx(typeof v === 'function' ? v(el.className) : v).split(' ')
|
|
17
|
+
if (v) for (let c of clsx(typeof v === 'function' ? v(el.className) : v).split(' ')) c && _new.add(c)
|
|
18
18
|
for (let c of _cur) if (!_new.has(c)) el.classList.remove(c);
|
|
19
19
|
for (let c of _new) if (!_cur.has(c)) el.classList.add(c);
|
|
20
20
|
if (!el.classList.length) el.removeAttribute('class')
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parse, decorate, _dispose } from "../core.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Intersect directive - IntersectionObserver wrapper.
|
|
5
|
+
* Statement form fires on enter. Function form receives entry for full control.
|
|
6
|
+
*
|
|
7
|
+
* :intersect="visible = true"
|
|
8
|
+
* :intersect.once="loadImage()"
|
|
9
|
+
* :intersect="entry => visible = entry.isIntersecting"
|
|
10
|
+
*
|
|
11
|
+
* @param {Element} el - Target element
|
|
12
|
+
* @param {Object} state - State object
|
|
13
|
+
* @param {string} expr - Handler expression
|
|
14
|
+
* @param {string} name - Directive name with modifiers
|
|
15
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
16
|
+
*/
|
|
17
|
+
const intersect = (el, state, expr, name) => {
|
|
18
|
+
const [, ...mods] = name.split('.')
|
|
19
|
+
const evaluate = parse(expr).bind(el)
|
|
20
|
+
|
|
21
|
+
let once = mods.includes('once')
|
|
22
|
+
|
|
23
|
+
const trigger = decorate(Object.assign((entry) => {
|
|
24
|
+
evaluate(state, fn => typeof fn === 'function' ? fn(entry) : fn)
|
|
25
|
+
}, { target: el }), mods)
|
|
26
|
+
|
|
27
|
+
const io = new IntersectionObserver(entries => {
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.isIntersecting) {
|
|
30
|
+
trigger(entry)
|
|
31
|
+
if (once) io.disconnect()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
io.observe(el)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
[_dispose]() { io.disconnect() }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
intersect.observer = true
|
|
44
|
+
export default intersect
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { parse, decorate, _dispose } from "../core.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mount directive - lifecycle observer.
|
|
5
|
+
* Runs once on connect. Function form receives element, can return cleanup.
|
|
6
|
+
* Statement form runs directly.
|
|
7
|
+
*
|
|
8
|
+
* :mount="console.log('connected')"
|
|
9
|
+
* :mount="el => (setup(el), () => cleanup(el))"
|
|
10
|
+
* :mount="el => ref = el"
|
|
11
|
+
*
|
|
12
|
+
* @param {Element} el - Target element
|
|
13
|
+
* @param {Object} state - State object
|
|
14
|
+
* @param {string} expr - Handler expression
|
|
15
|
+
* @param {string} name - Directive name with modifiers
|
|
16
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
17
|
+
*/
|
|
18
|
+
export default (el, state, expr, name) => {
|
|
19
|
+
const [, ...mods] = name.split('.'),
|
|
20
|
+
evaluate = parse(expr).bind(el)
|
|
21
|
+
|
|
22
|
+
let cleanup
|
|
23
|
+
|
|
24
|
+
const trigger = decorate(Object.assign(() => {
|
|
25
|
+
const result = evaluate(state, fn => typeof fn === 'function' ? fn(el) : fn)
|
|
26
|
+
if (typeof result === 'function') cleanup = result
|
|
27
|
+
}, { target: el }), mods)
|
|
28
|
+
|
|
29
|
+
trigger()
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
[_dispose]() {
|
|
33
|
+
cleanup?.()
|
|
34
|
+
cleanup = null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/directive/ref.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { parse } from "../core.js"
|
|
2
|
-
import { setter } from "./value.js"
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Creates a setter function for assigning a value to a state path.
|
|
5
|
+
* @param {string} expr - Expression to assign to (e.g., "x" or "refs.el")
|
|
6
|
+
* @returns {(target: Object, value: any) => void} Setter function
|
|
7
|
+
*/
|
|
8
|
+
const setter = (expr, _set = parse(`${expr}=__`)) => (target, value) => {
|
|
9
|
+
target.__ = value; _set(target), delete target.__
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ref directive - stores element reference in state or calls callback with element.
|
|
14
|
+
*
|
|
15
|
+
* :ref="x" → state.x = el
|
|
16
|
+
* :ref="refs.el" → state.refs.el = el
|
|
17
|
+
* :ref="el => setup(el)" → calls callback with element
|
|
18
|
+
*
|
|
7
19
|
* @param {Element} el - Target element
|
|
8
20
|
* @param {Object} state - State object
|
|
9
|
-
* @param {string} expr - Variable name or
|
|
10
|
-
* @returns {{ [Symbol.dispose]: () => void } | void} Disposal object or void
|
|
21
|
+
* @param {string} expr - Variable name, path, or callback expression
|
|
11
22
|
*/
|
|
12
23
|
export default (el, state, expr) => {
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// NOTE: we have to set element statically (outside of effect) to avoid parasitic sub - multiple els with same :ref can cause recursion (eg. :each :ref="x")
|
|
18
|
-
// Object.defineProperty(state, expr, { value: el, configurable: true })
|
|
19
|
-
setter(expr)(state, el)
|
|
24
|
+
let result = parse(expr).call(el, state)
|
|
25
|
+
if (typeof result === 'function') result(el)
|
|
26
|
+
else setter(expr)(state, el)
|
|
20
27
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { parse, decorate, _dispose } from "../core.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resize directive - ResizeObserver wrapper.
|
|
5
|
+
* Function form receives {width, height, entry} object.
|
|
6
|
+
*
|
|
7
|
+
* :resize="({width, height}) => cols = Math.floor(width / 200)"
|
|
8
|
+
* :resize.throttle-100="({width}) => narrow = width < 600"
|
|
9
|
+
*
|
|
10
|
+
* @param {Element} el - Target element
|
|
11
|
+
* @param {Object} state - State object
|
|
12
|
+
* @param {string} expr - Handler expression
|
|
13
|
+
* @param {string} name - Directive name with modifiers
|
|
14
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
15
|
+
*/
|
|
16
|
+
const resize = (el, state, expr, name) => {
|
|
17
|
+
const [, ...mods] = name.split('.')
|
|
18
|
+
const evaluate = parse(expr).bind(el)
|
|
19
|
+
|
|
20
|
+
const trigger = decorate(Object.assign((size) => {
|
|
21
|
+
evaluate(state, fn => typeof fn === 'function' ? fn(size) : fn)
|
|
22
|
+
}, { target: el }), mods)
|
|
23
|
+
|
|
24
|
+
const ro = new ResizeObserver(entries => {
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const rect = entry.contentRect
|
|
27
|
+
trigger({ width: rect.width, height: rect.height, entry })
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
ro.observe(el)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
[_dispose]() { ro.disconnect() }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resize.observer = true
|
|
39
|
+
export default resize
|
package/directive/sequence.js
CHANGED
|
@@ -13,12 +13,12 @@ export default (el, state, expr, names) => {
|
|
|
13
13
|
let cur, // current step callback
|
|
14
14
|
off // current step disposal
|
|
15
15
|
|
|
16
|
+
const evaluate = parse(expr).bind(el)
|
|
17
|
+
|
|
16
18
|
let steps = names.split('..').map((step, i, { length }) => step.split(':').reduce(
|
|
17
19
|
(prev, str) => {
|
|
18
20
|
const [name, ...mods] = str.slice(2).split('.')
|
|
19
21
|
|
|
20
|
-
const evaluate = parse(expr).bind(el)
|
|
21
|
-
|
|
22
22
|
const next = (fn, e) => cur = typeof fn === 'function' ? fn(e) : fn
|
|
23
23
|
const trigger = decorate(Object.assign(
|
|
24
24
|
e => (!i ? evaluate(state, (fn) => next(fn, e)) : next(cur, e), off(), off = steps[(i + 1) % length]()),
|
package/directive/spread.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attr, dashcase } from "../core.js";
|
|
1
|
+
import { attr, dashcase, isCE } from "../core.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Spread directive - sets multiple attributes from object.
|
|
@@ -6,4 +6,4 @@ import { attr, dashcase } from "../core.js";
|
|
|
6
6
|
* @param {Element} target - Target element
|
|
7
7
|
* @returns {(value: Record<string, any>) => void} Update function
|
|
8
8
|
*/
|
|
9
|
-
export default (target) => value => { for (let key in value) attr(target, dashcase(key), value[key]) }
|
|
9
|
+
export default (target) => value => { let ce = isCE(target); for (let key in value) attr(target, ce ? key : dashcase(key), value[key]) }
|
package/directive/style.js
CHANGED
|
@@ -19,7 +19,7 @@ export default (el, st, ex, name) => {
|
|
|
19
19
|
else {
|
|
20
20
|
if (_static) attr(el, "style", _static);
|
|
21
21
|
// NOTE: we skip names not starting with a letter - eg. el.style stores properties as { 0: --x } or JSDOM has _pfx
|
|
22
|
-
for (let k in v) k[0] == '-' ? el.style.setProperty(k, v[k]) : k[0]
|
|
22
|
+
for (let k in v) k[0] == '-' ? el.style.setProperty(k, v[k]) : k[0] >= 'A' && (el.style[k] = v[k])
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
}
|
package/directive/text.js
CHANGED
|
@@ -2,11 +2,32 @@ import { frag } from "../core.js"
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Text directive - sets textContent reactively.
|
|
5
|
+
* Preserves caret/selection position across updates.
|
|
5
6
|
* @param {Element | HTMLTemplateElement} el - Target element
|
|
6
7
|
* @returns {(v: any) => void} Update function
|
|
7
8
|
*/
|
|
8
9
|
export default el => (
|
|
9
10
|
// <template :text="a"/> or previously initialized template
|
|
10
11
|
el.content && el.replaceWith(el = frag(el).childNodes[0]),
|
|
11
|
-
v =>
|
|
12
|
+
v => {
|
|
13
|
+
v = typeof v === 'function' ? v(el.textContent) : v
|
|
14
|
+
v = v == null ? "" : v
|
|
15
|
+
if (el.textContent === v) return
|
|
16
|
+
|
|
17
|
+
// save caret position
|
|
18
|
+
let s = el.getRootNode().getSelection?.()
|
|
19
|
+
let off = s?.rangeCount && el.contains(s.anchorNode) ? s.getRangeAt(0).startOffset : -1
|
|
20
|
+
|
|
21
|
+
el.textContent = v
|
|
22
|
+
|
|
23
|
+
// restore caret
|
|
24
|
+
if (off >= 0 && el.firstChild) {
|
|
25
|
+
let pos = Math.min(off, el.firstChild.textContent.length)
|
|
26
|
+
let r = new Range()
|
|
27
|
+
r.setStart(el.firstChild, pos)
|
|
28
|
+
r.collapse(true)
|
|
29
|
+
s.removeAllRanges()
|
|
30
|
+
s.addRange(r)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
12
33
|
)
|
package/directive/value.js
CHANGED
|
@@ -1,42 +1,17 @@
|
|
|
1
|
-
import sprae, { attr,
|
|
1
|
+
import sprae, { attr, _dispose } from "../core.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
export const setter = (expr, _set = parse(`${expr}=__`)) => (target, value) => {
|
|
9
|
-
target.__ = value; _set(target), delete target.__
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Value directive - two-way binding for form elements.
|
|
14
|
-
* Supports text, checkbox, radio, select, and textarea.
|
|
4
|
+
* Value directive - one-way binding (state → DOM).
|
|
5
|
+
* Sets element value/checked/selected from state.
|
|
6
|
+
* For write-back (DOM → state), use :change directive.
|
|
7
|
+
*
|
|
15
8
|
* @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} el - Form element
|
|
16
9
|
* @param {Object} state - State object
|
|
17
|
-
* @param {string} expr - Bound expression
|
|
18
10
|
* @returns {(value: any) => void} Update function
|
|
19
11
|
*/
|
|
20
|
-
export default (el, state
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const handleChange = el.type === 'checkbox' ? () => set(state, el.checked) :
|
|
24
|
-
el.type === 'select-multiple' ? () => set(state, [...el.selectedOptions].map(o => o.value)) :
|
|
25
|
-
/^(date|time|month|week)/.test(el.type) ? () => set(state, el.value) :
|
|
26
|
-
() => set(state, el.selectedIndex < 0 ? null : isNaN(el.valueAsNumber) ? el.value : el.valueAsNumber);
|
|
27
|
-
|
|
28
|
-
el.oninput = el.onchange = handleChange;
|
|
29
|
-
|
|
30
|
-
if (el.type?.startsWith('select')) {
|
|
31
|
-
let mo = new MutationObserver(() => handleChange())
|
|
32
|
-
mo.observe(el, { childList: true, subtree: true, attributes: true });
|
|
33
|
-
sprae(el, state)
|
|
34
|
-
let _prevDispose = el[_dispose]
|
|
35
|
-
if (_prevDispose) el[_dispose] = () => { mo?.disconnect(); mo = null; _prevDispose() }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
parse(expr)(state) ?? handleChange()
|
|
39
|
-
} catch { }
|
|
12
|
+
export default (el, state) => {
|
|
13
|
+
// select elements need children spraed first (for :each options)
|
|
14
|
+
if (el.type?.startsWith('select')) sprae(el, state)
|
|
40
15
|
|
|
41
16
|
return (el.type === "text" || el.type === "" || el.tagName === "TEXTAREA") ?
|
|
42
17
|
(value, _from, _to) => (
|
|
@@ -48,7 +23,7 @@ export default (el, state, expr) => {
|
|
|
48
23
|
(el.type === "checkbox") ?
|
|
49
24
|
(value) => (el.checked = value, attr(el, "checked", value)) :
|
|
50
25
|
(el.type === 'radio') ? (value) => (
|
|
51
|
-
el.
|
|
26
|
+
el.checked = el.value === value, attr(el, 'checked', el.checked || null)
|
|
52
27
|
) :
|
|
53
28
|
(el.type === "select-one") ?
|
|
54
29
|
(value) => {
|
|
@@ -57,8 +32,7 @@ export default (el, state, expr) => {
|
|
|
57
32
|
el.value = value;
|
|
58
33
|
} :
|
|
59
34
|
(el.type === 'select-multiple') ? (value) => {
|
|
60
|
-
for (let o of el.options) o.removeAttribute('selected')
|
|
61
|
-
for (let v of value) el.querySelector(`[value="${v}"]`).setAttribute('selected', '')
|
|
35
|
+
for (let o of el.options) value.some(v => v == o.value) ? o.setAttribute('selected', '') : o.removeAttribute('selected')
|
|
62
36
|
} :
|
|
63
|
-
(value) => (el.value = value)
|
|
37
|
+
(value) => (el.value = value)
|
|
64
38
|
}
|