sprae 11.6.0 → 12.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 +229 -90
- package/directive/class.js +9 -13
- package/directive/default.js +2 -154
- package/directive/each.js +79 -75
- package/directive/else.js +22 -0
- package/directive/fx.js +2 -2
- package/directive/if.js +40 -34
- package/directive/ref.js +8 -7
- package/directive/scope.js +17 -0
- package/directive/spread.js +3 -0
- package/directive/style.js +9 -7
- package/directive/text.js +4 -4
- package/directive/value.js +39 -40
- package/dist/sprae.js +3 -495
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.umd.js +3 -640
- package/dist/sprae.umd.js.map +4 -4
- package/micro.js +2 -0
- package/package.json +17 -14
- package/readme.md +432 -205
- package/signal.js +41 -40
- package/sprae.js +127 -18
- package/store.js +109 -96
- package/directive/aria.js +0 -6
- package/directive/data.js +0 -3
- package/directive/with.js +0 -12
- package/dist/sprae.auto.js +0 -662
- package/dist/sprae.auto.js.map +0 -7
- package/dist/sprae.auto.min.js +0 -5
- package/dist/sprae.auto.min.js.map +0 -7
- package/dist/sprae.min.js +0 -5
- package/dist/sprae.min.js.map +0 -7
- package/dist/sprae.umd.min.js +0 -5
- package/dist/sprae.umd.min.js.map +0 -7
package/directive/each.js
CHANGED
|
@@ -1,86 +1,90 @@
|
|
|
1
|
-
import sprae, { _state,
|
|
2
|
-
import store, { _change, _signals } from "../store.js";
|
|
3
|
-
import { effect } from '../signal.js';
|
|
1
|
+
import sprae, { store, _state, effect, _change, _signals, frag, throttle } from "../core.js";
|
|
4
2
|
|
|
3
|
+
const each = (tpl, state, expr) => {
|
|
4
|
+
let [itemVar, idxVar = "$"] = expr.split(/\bin\b/)[0].trim().replace(/\(|\)/g, '').split(/\s*,\s*/);
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// we need :if to be able to replace holder instead of tpl for :if :each case
|
|
7
|
+
let holder = document.createTextNode("");
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// we re-create items any time new items are produced
|
|
10
|
+
let cur, keys, items, prevl = 0
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// FIXME: pass items to update instead of global
|
|
13
|
+
let update = throttle(() => {
|
|
14
|
+
let i = 0, newItems = items, newl = newItems.length
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// plain array update, not store (signal with array) - updates full list
|
|
17
|
+
if (cur && !cur[_change]) {
|
|
18
|
+
for (let s of cur[_signals] || []) s[Symbol.dispose]()
|
|
19
|
+
cur = null, prevl = 0
|
|
20
|
+
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
// delete
|
|
23
|
+
if (newl < prevl) cur.length = newl
|
|
24
|
+
|
|
25
|
+
// update, append, init
|
|
26
|
+
else {
|
|
27
|
+
// init
|
|
28
|
+
if (!cur) cur = newItems
|
|
29
|
+
// update
|
|
30
|
+
else while (i < prevl) cur[i] = newItems[i++]
|
|
31
|
+
|
|
32
|
+
// append
|
|
33
|
+
for (; i < newl; i++) {
|
|
34
|
+
cur[i] = newItems[i]
|
|
35
|
+
|
|
36
|
+
let idx = i,
|
|
37
|
+
// inherited state must be cheaper in terms of memory and faster in terms of performance, compared to creating a proxy store
|
|
38
|
+
// subscope = store({
|
|
39
|
+
// // NOTE: since we simulate signal, we have to make sure it's actual signal, not fake one
|
|
40
|
+
// // FIXME: try to avoid this, we also have issue with wrongly calling dispose in store on delete
|
|
41
|
+
// [itemVar]: cur[_signals]?.[idx]?.peek ? cur[_signals]?.[idx] : cur[idx],
|
|
42
|
+
// [idxVar]: keys ? keys[idx] : idx
|
|
43
|
+
// }, state)
|
|
44
|
+
subscope = Object.create(state, {
|
|
45
|
+
[itemVar]: { get: () => cur[idx] },
|
|
46
|
+
[idxVar]: { value: keys ? keys[idx] : idx }
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
let el = tpl.content ? frag(tpl) : tpl.cloneNode(true);
|
|
50
|
+
|
|
51
|
+
holder.before(el.content || el);
|
|
52
|
+
|
|
53
|
+
sprae(el, subscope);
|
|
54
|
+
|
|
55
|
+
// signal/holder disposal removes element
|
|
56
|
+
let _prev = ((cur[_signals] ||= [])[i] ||= {})[Symbol.dispose]
|
|
57
|
+
cur[_signals][i][Symbol.dispose] = () => {
|
|
58
|
+
_prev?.(), el[Symbol.dispose]?.(), el.remove()
|
|
59
|
+
};
|
|
22
60
|
}
|
|
61
|
+
}
|
|
23
62
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// update, append, init
|
|
28
|
-
else {
|
|
29
|
-
// init
|
|
30
|
-
if (!cur) cur = newItems
|
|
31
|
-
// update
|
|
32
|
-
else while (i < prevl) cur[i] = newItems[i++]
|
|
33
|
-
|
|
34
|
-
// append
|
|
35
|
-
for (; i < newl; i++) {
|
|
36
|
-
cur[i] = newItems[i]
|
|
37
|
-
let idx = i,
|
|
38
|
-
// FIXME: inherited state is cheaper in terms of memory and faster in terms of performance
|
|
39
|
-
// compared to cloning all parent signals and creating a proxy
|
|
40
|
-
// FIXME: besides try to avoid _signals access: we can optimize store then not checking for _signals key
|
|
41
|
-
scope = store({
|
|
42
|
-
[itemVar]: cur[_signals]?.[idx] || cur[idx],
|
|
43
|
-
[idxVar]: keys ? keys[idx] : idx
|
|
44
|
-
}, state),
|
|
45
|
-
|
|
46
|
-
el = tpl.content ? frag(tpl) : tpl.cloneNode(true);
|
|
47
|
-
|
|
48
|
-
holder.before(el.content || el);
|
|
49
|
-
sprae(el, scope);
|
|
50
|
-
|
|
51
|
-
// signal/holder disposal removes element
|
|
52
|
-
let _prev = ((cur[_signals] ||= [])[i] ||= {})[Symbol.dispose]
|
|
53
|
-
cur[_signals][i][Symbol.dispose] = () => {
|
|
54
|
-
_prev?.(), el[Symbol.dispose]?.(), el.remove()
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
63
|
+
prevl = newl
|
|
64
|
+
})
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
tpl.replaceWith(holder);
|
|
67
|
+
tpl[_state] = null // mark as fake-spraed, to preserve :-attribs for template
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
},
|
|
69
|
+
return value => {
|
|
70
|
+
// resolve new items
|
|
71
|
+
keys = null
|
|
72
|
+
if (typeof value === "number") items = Array.from({ length: value }, (_, i) => i + 1)
|
|
73
|
+
else if (value?.constructor === Object) keys = Object.keys(value), items = Object.values(value)
|
|
74
|
+
else items = value || []
|
|
75
|
+
|
|
76
|
+
// whenever list changes, we rebind internal change effect
|
|
77
|
+
return effect(() => {
|
|
78
|
+
// subscribe to items change (.length) - we do it every time (not just in update) since preact unsubscribes unused signals
|
|
79
|
+
items[_change]?.value
|
|
80
|
+
|
|
81
|
+
// make first render immediately, debounce subsequent renders
|
|
82
|
+
update()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// :each directive skips v, k
|
|
88
|
+
each.parse = (str) => str.split(/\bin\b/)[1].trim()
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
expr => parse(expr.split(/\bin\b/)[1])
|
|
86
|
-
)
|
|
90
|
+
export default each
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { _on, _off, _state, frag } from '../core.js';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// NOTE: we can reach :else counterpart whereas prev :else :if is on hold
|
|
5
|
+
export default (el, state, _el, _, _prev=el) => {
|
|
6
|
+
|
|
7
|
+
// console.log(':else init', el)
|
|
8
|
+
_el = el.content ? frag(el) : el
|
|
9
|
+
|
|
10
|
+
// find holder
|
|
11
|
+
while (_prev && !(_el._holder = _prev._holder)) _prev = _prev.previousSibling
|
|
12
|
+
|
|
13
|
+
el.remove()
|
|
14
|
+
el[_state] = null // mark as fake-spraed to stop further init, to lazy-sprae when branch matches
|
|
15
|
+
|
|
16
|
+
_el._holder._clauses.push(_el._clause = [_el, true])
|
|
17
|
+
|
|
18
|
+
return() => {
|
|
19
|
+
// console.log(':else update', _el)
|
|
20
|
+
_el._holder.update()
|
|
21
|
+
}
|
|
22
|
+
}
|
package/directive/fx.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { call } from "../core.js"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export default () => v => (call(v))
|
package/directive/if.js
CHANGED
|
@@ -1,41 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
// "centralized" version of :if
|
|
2
|
+
import sprae, { throttle, _on, _off, _state, frag } from '../core.js';
|
|
2
3
|
|
|
3
|
-
// :if
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
4
|
+
// :if="a"
|
|
5
|
+
export default (el, state, _holder, _el, _match) => {
|
|
6
|
+
// new element :if
|
|
7
|
+
// console.log(':if init', el)
|
|
8
|
+
if (!el._holder) {
|
|
9
|
+
// mark el as fake-spraed to delay init, since we sprae rest when branch matches, both :if and :else :if
|
|
10
|
+
el[_state] ??= null
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
let holder = document.createTextNode('')
|
|
12
|
+
_el = el.content ? frag(el) : el
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
el.replaceWith(_holder = document.createTextNode(''))
|
|
15
|
+
_el._holder = _holder._holder = _holder
|
|
13
16
|
|
|
14
|
-
el.replaceWith(holder)
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
ifEl[_state] = null // mark el as fake-spraed to hold-on init, since we sprae rest when branch matches
|
|
18
|
+
_holder._clauses = [_el._clause = [_el, false]]
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (curEl = newEl) {
|
|
33
|
-
holder.before(curEl.content || curEl)
|
|
34
|
-
// remove fake memo to sprae as new
|
|
35
|
-
curEl[_state] === null ? (delete curEl[_state], sprae(curEl, state))
|
|
36
|
-
// enable effects if branch is matched
|
|
37
|
-
: curEl[_on]()
|
|
20
|
+
_holder.update = throttle(() => {
|
|
21
|
+
let match = _holder._clauses.find(([, s]) => s)
|
|
22
|
+
// console.group(':if update clauses', ..._holder._clauses)
|
|
23
|
+
|
|
24
|
+
if (match != _match) {
|
|
25
|
+
// console.log(':if match', match)
|
|
26
|
+
_match?.[0].remove()
|
|
27
|
+
_match?.[0][_off]?.()
|
|
28
|
+
if (_match = match) {
|
|
29
|
+
_holder.before(_match[0].content || _match[0])
|
|
30
|
+
// there's no :else after :if, so lazy-sprae here doesn't risk adding own destructor to own list of destructors
|
|
31
|
+
!_match[0][_state] ? (delete _match[0][_state], sprae(_match[0], state)) : _match[0][_on]?.()
|
|
32
|
+
}
|
|
38
33
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
34
|
+
// console.groupEnd()
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
// :else :if needs to be spraed all over to have clean list of offable effects
|
|
38
|
+
else sprae(_el = el, state)
|
|
39
|
+
|
|
40
|
+
// :else may have children to init which is called after :if
|
|
41
|
+
// or preact can schedule :else after :if, so we ensure order of call by next tick
|
|
42
|
+
return value => {
|
|
43
|
+
// console.log(':if update', _el, value)
|
|
44
|
+
_el._clause[1] = value
|
|
45
|
+
_el._holder.update()
|
|
46
|
+
}
|
|
47
|
+
}
|
package/directive/ref.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { setter } from "../store.js";
|
|
1
|
+
import { parse } from "../core.js"
|
|
2
|
+
// import { setter } from "./value.js"
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
export default (el, state, expr, name, _prev, _set) => (
|
|
6
5
|
typeof parse(expr)(state) == 'function' ?
|
|
7
|
-
v => v
|
|
8
|
-
(
|
|
9
|
-
))
|
|
6
|
+
v => (v(el)) :
|
|
7
|
+
// 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")
|
|
8
|
+
// (setter(expr)(state, el))
|
|
9
|
+
(Object.defineProperty(state, expr, { value: el, configurable: true }), () => {})
|
|
10
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sprae, { store, call, untracked, _state } from '../core.js'
|
|
2
|
+
|
|
3
|
+
export default (el, rootState, _scope) => (
|
|
4
|
+
// prevent subsequent effects
|
|
5
|
+
el[_state] = null,
|
|
6
|
+
// 0 run pre-creates state to provide scope for the first effect - it can write vars in it, so we should already have it
|
|
7
|
+
_scope = store({}, rootState),
|
|
8
|
+
// 1st run spraes subtree with values from scope - it can be postponed by modifiers (we isolate reads from parent effect)
|
|
9
|
+
// 2nd+ runs update _scope
|
|
10
|
+
values => {
|
|
11
|
+
let ext = call(values, _scope);
|
|
12
|
+
// we bind to _scope to alleviate friction of using scope method directly
|
|
13
|
+
for (let k in ext) _scope[k] = typeof ext[k] === 'function' ? ext[k].bind(_scope) : ext[k];
|
|
14
|
+
// Object.assign(_scope, call(values, _scope))
|
|
15
|
+
return el[_state] ?? (delete el[_state], untracked(() => sprae(el, _scope)))
|
|
16
|
+
}
|
|
17
|
+
)
|
package/directive/style.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { call, attr } from "../core.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
export default (el, _static) => (
|
|
4
|
+
_static = el.getAttribute("style"),
|
|
5
5
|
v => {
|
|
6
|
-
|
|
6
|
+
v = call(v, el.style)
|
|
7
|
+
if (typeof v === "string") attr(el, "style", _static + '; ' + v);
|
|
7
8
|
else {
|
|
8
|
-
if (
|
|
9
|
-
|
|
9
|
+
if (_static) attr(el, "style", _static);
|
|
10
|
+
// NOTE: we skip names not starting with a letter - eg. el.style stores properties as { 0: --x } or JSDOM has _pfx
|
|
11
|
+
for (let k in v) k[0] == '-' ? el.style.setProperty(k, v[k]) : k[0] > 'A' && (el.style[k] = v[k])
|
|
10
12
|
}
|
|
11
|
-
}
|
|
13
|
+
}
|
|
12
14
|
)
|
package/directive/text.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { frag, call } from "../core.js"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export default el => (
|
|
4
4
|
// <template :text="a"/> or previously initialized template
|
|
5
5
|
el.content && el.replaceWith(el = frag(el).childNodes[0]),
|
|
6
|
-
|
|
7
|
-
)
|
|
6
|
+
v => (v = call(v, el.textContent), el.textContent = v == null ? "" : v)
|
|
7
|
+
)
|
package/directive/value.js
CHANGED
|
@@ -1,39 +1,13 @@
|
|
|
1
|
-
import sprae, { parse } from "../core.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const update =
|
|
10
|
-
(el.type === "text" || el.type === "") ?
|
|
11
|
-
(value) => el.setAttribute("value", (el.value = value == null ? "" : value)) :
|
|
12
|
-
(el.tagName === "TEXTAREA" || el.type === "text" || el.type === "") ?
|
|
13
|
-
(value, from, to) => (
|
|
14
|
-
// we retain selection in input
|
|
15
|
-
(from = el.selectionStart),
|
|
16
|
-
(to = el.selectionEnd),
|
|
17
|
-
el.setAttribute("value", (el.value = value == null ? "" : value)),
|
|
18
|
-
from && el.setSelectionRange(from, to)
|
|
19
|
-
) :
|
|
20
|
-
(el.type === "checkbox") ?
|
|
21
|
-
(value) => (el.checked = value, attr(el, "checked", value)) :
|
|
22
|
-
el.type === 'radio' ? (value) => (
|
|
23
|
-
el.value === value && ((el.checked = value), attr(el, 'checked', value))
|
|
24
|
-
) :
|
|
25
|
-
(el.type === "select-one") ?
|
|
26
|
-
(value) => {
|
|
27
|
-
for (let o of el.options)
|
|
28
|
-
o.value == value ? o.setAttribute("selected", '') : o.removeAttribute("selected");
|
|
29
|
-
el.value = value;
|
|
30
|
-
} :
|
|
31
|
-
(el.type === 'select-multiple') ? (value) => {
|
|
32
|
-
for (let o of el.options) o.removeAttribute('selected')
|
|
33
|
-
for (let v of value) el.querySelector(`[value="${v}"]`).setAttribute('selected', '')
|
|
34
|
-
} :
|
|
35
|
-
(value) => (el.value = value);
|
|
1
|
+
import sprae, { attr, parse, _state } from "../core.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// create expression setter, reflecting value back to state
|
|
5
|
+
export const setter = (expr, _set = parse(`${expr}=__`)) => (target, value) => {
|
|
6
|
+
// save value to stash
|
|
7
|
+
target.__ = value; _set(target), delete target.__
|
|
8
|
+
}
|
|
36
9
|
|
|
10
|
+
export default (el, state, expr, name) => {
|
|
37
11
|
// bind back to value, but some values can be not bindable, eg. `:value="7"`
|
|
38
12
|
try {
|
|
39
13
|
const set = setter(expr)
|
|
@@ -51,9 +25,34 @@ dir('value', (el, state, expr) => {
|
|
|
51
25
|
sprae(el, state)
|
|
52
26
|
}
|
|
53
27
|
|
|
54
|
-
// initial state value
|
|
28
|
+
// initial state value - setter has already cached it, no need to parse again
|
|
55
29
|
parse(expr)(state) ?? handleChange()
|
|
56
|
-
} catch {}
|
|
57
|
-
|
|
58
|
-
return
|
|
59
|
-
|
|
30
|
+
} catch { }
|
|
31
|
+
|
|
32
|
+
return (el.type === "text" || el.type === "") ?
|
|
33
|
+
(value) => el.setAttribute("value", (el.value = value == null ? "" : value)) :
|
|
34
|
+
(el.tagName === "TEXTAREA" || el.type === "text" || el.type === "") ?
|
|
35
|
+
(value, from, to) => (
|
|
36
|
+
// we retain selection in input
|
|
37
|
+
(from = el.selectionStart),
|
|
38
|
+
(to = el.selectionEnd),
|
|
39
|
+
el.setAttribute("value", (el.value = value == null ? "" : value)),
|
|
40
|
+
from && el.setSelectionRange(from, to)
|
|
41
|
+
) :
|
|
42
|
+
(el.type === "checkbox") ?
|
|
43
|
+
(value) => (el.checked = value, attr(el, "checked", value)) :
|
|
44
|
+
(el.type === 'radio') ? (value) => (
|
|
45
|
+
el.value === value && ((el.checked = value), attr(el, 'checked', value))
|
|
46
|
+
) :
|
|
47
|
+
(el.type === "select-one") ?
|
|
48
|
+
(value) => {
|
|
49
|
+
for (let o of el.options)
|
|
50
|
+
o.value == value ? o.setAttribute("selected", '') : o.removeAttribute("selected");
|
|
51
|
+
el.value = value;
|
|
52
|
+
} :
|
|
53
|
+
(el.type === 'select-multiple') ? (value) => {
|
|
54
|
+
for (let o of el.options) o.removeAttribute('selected')
|
|
55
|
+
for (let v of value) el.querySelector(`[value="${v}"]`).setAttribute('selected', '')
|
|
56
|
+
} :
|
|
57
|
+
(value) => (el.value = value);
|
|
58
|
+
}
|