sprae 11.6.0 → 12.0.0

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/directive/each.js CHANGED
@@ -1,86 +1,90 @@
1
- import sprae, { _state, dir, frag, parse } from "../core.js";
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
- dir('each', (tpl, state, expr) => {
7
- let [itemVar, idxVar = "$"] = expr.split(/\bin\b/)[0].trim().split(/\s*,\s*/);
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
- // we need :if to be able to replace holder instead of tpl for :if :each case
10
- let holder = document.createTextNode("");
9
+ // we re-create items any time new items are produced
10
+ let cur, keys, items, prevl = 0
11
11
 
12
- // we re-create items any time new items are produced
13
- let cur, keys, items, prevl = 0
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
- let update = () => {
16
- let i = 0, newItems = items, newl = newItems.length
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
- // plain array update, not store (signal with array) - updates full list
19
- if (cur && !cur[_change]) {
20
- for (let s of cur[_signals] || []) s[Symbol.dispose]()
21
- cur = null, prevl = 0
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
- // delete
25
- if (newl < prevl) cur.length = newl
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
- prevl = newl
60
- }
66
+ tpl.replaceWith(holder);
67
+ tpl[_state] = null // mark as fake-spraed, to preserve :-attribs for template
61
68
 
62
- tpl.replaceWith(holder);
63
- tpl[_state] = null // mark as fake-spraed, to preserve :-attribs for template
64
-
65
- return value => {
66
- // obtain new items
67
- keys = null
68
- if (typeof value === "number") items = Array.from({ length: value }, (_, i) => i + 1)
69
- else if (value?.constructor === Object) keys = Object.keys(value), items = Object.values(value)
70
- else items = value || []
71
-
72
- // whenever list changes, we rebind internal change effect
73
- let planned = 0
74
- return effect(() => {
75
- // subscribe to items change (.length) - we do it every time (not just in update) since preact unsubscribes unused signals
76
- items[_change]?.value
77
-
78
- // make first render immediately, debounce subsequent renders
79
- if (!planned++) update(), queueMicrotask(() => (planned > 1 && update(), planned = 0));
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
- // redefine evaluator to take second part of expression
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 { dir } from "../core.js";
1
+ import { call } from "../core.js"
2
2
 
3
- dir('fx', _ => _ => _)
3
+ export default () => v => (call(v))
package/directive/if.js CHANGED
@@ -1,41 +1,47 @@
1
- import sprae, { dir, _state, _on, _off, frag } from "../core.js";
1
+ // "centralized" version of :if
2
+ import sprae, { throttle, _on, _off, _state, frag } from '../core.js';
2
3
 
3
- // :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
4
- // as for :if :with - :if must init first, since it is lazy, to avoid initializing component ahead of time by :with
5
- // we consider :with={x} :if={x} case insignificant
6
- const _prevIf = Symbol("if");
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
- dir('if', (el, state) => {
9
- let holder = document.createTextNode('')
12
+ _el = el.content ? frag(el) : el
10
13
 
11
- let nextEl = el.nextElementSibling,
12
- curEl, ifEl, elseEl;
14
+ el.replaceWith(_holder = document.createTextNode(''))
15
+ _el._holder = _holder._holder = _holder
13
16
 
14
- el.replaceWith(holder)
15
17
 
16
- ifEl = el.content ? frag(el) : el
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
- // FIXME: instead of nextEl / el we should use elseEl / ifEl
20
- if (nextEl?.hasAttribute(":else")) {
21
- nextEl.removeAttribute(":else");
22
- // if nextEl is :else :if - leave it for its own :if handler
23
- if (!nextEl.hasAttribute(":if")) nextEl.remove(), elseEl = nextEl.content ? frag(nextEl) : nextEl, elseEl[_state] = null
24
- }
25
- else nextEl = null
26
-
27
- return (value, newEl = el[_prevIf] ? null : value ? ifEl : elseEl) => {
28
- if (nextEl) nextEl[_prevIf] = el[_prevIf] || newEl == ifEl
29
- if (curEl != newEl) {
30
- // disable effects on child elements when element is not matched
31
- if (curEl) curEl.remove(), curEl[_off]?.();
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 { dir, parse } from "../core.js";
2
- import { untracked } from "../signal.js";
3
- import { setter } from "../store.js";
1
+ import { parse } from "../core.js"
2
+ // import { setter } from "./value.js"
4
3
 
5
- dir('ref', (el, state, expr) => (
4
+ export default (el, state, expr, name, _prev, _set) => (
6
5
  typeof parse(expr)(state) == 'function' ?
7
- v => v.call(null, el) :
8
- (setter(expr)(state, el), _ => _)
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
+ )
@@ -0,0 +1,3 @@
1
+ import { attr, dashcase } from "../core.js";
2
+
3
+ export default (target) => value => { for (let key in value) attr(target, dashcase(key), value[key]) }
@@ -1,12 +1,14 @@
1
- import { dir } from "../core.js";
1
+ import { call, attr } from "../core.js";
2
2
 
3
- dir('style', (el, initStyle) => (
4
- initStyle = el.getAttribute("style"),
3
+ export default (el, _static) => (
4
+ _static = el.getAttribute("style"),
5
5
  v => {
6
- if (typeof v === "string") el.setAttribute("style", initStyle + (initStyle.endsWith(';') ? '' : '; ') + v);
6
+ v = call(v, el.style)
7
+ if (typeof v === "string") attr(el, "style", _static + '; ' + v);
7
8
  else {
8
- if (initStyle) el.setAttribute("style", initStyle);
9
- for (let k in v) k[0] == '-' ? (el.style.setProperty(k, v[k])) : el.style[k] = v[k]
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 { dir, frag } from "../core.js";
1
+ import { frag, call } from "../core.js"
2
2
 
3
- dir('text', el => (
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
- value => el.textContent = value == null ? "" : value
7
- ))
6
+ v => (v = call(v, el.textContent), el.textContent = v == null ? "" : v)
7
+ )
@@ -1,39 +1,13 @@
1
- import sprae, { parse } from "../core.js";
2
- import { dir } from "../core.js";
3
- import { untracked } from "../signal.js";
4
- import { setter } from "../store.js";
5
- import { attr } from './default.js';
6
-
7
-
8
- dir('value', (el, state, expr) => {
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 update
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
+ }