sprae 12.2.4 → 12.3.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/signal.js CHANGED
@@ -6,7 +6,7 @@ let current, depth = 0, batched;
6
6
  export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
7
7
  _s = {
8
8
  get value() {
9
- current?.deps.push(_obs.add(current));
9
+ current?.deps.add(_obs.add(current));
10
10
  return v
11
11
  },
12
12
  set value(val) {
@@ -15,23 +15,24 @@ export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
15
15
  for (let sub of _obs) batched ? batched.add(sub) : sub(); // notify effects
16
16
  },
17
17
  peek() { return v },
18
- toJSON: _v, then: _v, toString: _v, valueOf: _v
18
+ toJSON: _v, toString: _v, valueOf: _v
19
19
  }
20
20
  )
21
21
 
22
- export const effect = (fn, _teardown, _fx, _deps, __tmp) => (
22
+ export const effect = (fn, _teardown, _fx, _deps) => (
23
23
  _fx = (prev) => {
24
- __tmp = _teardown;
24
+ let tmp = _teardown;
25
25
  _teardown = null; // we null _teardown to avoid repeated call in case of recursive update
26
- __tmp?.call?.();
26
+ tmp?.call?.();
27
27
  prev = current, current = _fx
28
28
  if (depth++ > 10) throw 'Cycle detected';
29
- try { _teardown = fn(); } finally { current = prev; depth-- }
29
+ try { _teardown = fn() } finally { current = prev; depth-- }
30
30
  },
31
- _deps = _fx.deps = [],
31
+ _fx.fn = fn,
32
+ _deps = _fx.deps = new Set(),
32
33
 
33
34
  _fx(),
34
- (dep) => { _teardown?.call?.(); while (dep = _deps.pop()) dep.delete(_fx); }
35
+ (dep) => { _teardown?.call?.(); for (dep of _deps) dep.delete(_fx); _deps.clear() }
35
36
  )
36
37
 
37
38
  export const computed = (fn, _s = signal(), _c, _e, _v = () => _c.value) => (
@@ -41,14 +42,14 @@ export const computed = (fn, _s = signal(), _c, _e, _v = () => _c.value) => (
41
42
  return _s.value
42
43
  },
43
44
  peek: _s.peek,
44
- toJSON: _v, then: _v, toString: _v, valueOf: _v
45
+ toJSON: _v, toString: _v, valueOf: _v
45
46
  }
46
47
  )
47
48
 
48
- export const batch = (fn, _first = !batched) => {
49
+ export const batch = (fn, _first = !batched, _list) => {
49
50
  batched ??= new Set;
50
51
  try { fn(); }
51
- finally { if (_first) { for (const fx of batched) fx(); batched = null } }
52
+ finally { if (_first) { [batched, _list] = [null, batched]; for (const fx of _list) fx(); } }
52
53
  }
53
54
 
54
55
  export const untracked = (fn, _prev, _v) => (_prev = current, current = null, _v = fn(), current = _prev, _v)
package/sprae.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import store from "./store.js";
2
- import { batch, computed, effect, signal, untracked } from './signal.js';
3
- import sprae, { use, directive, modifier, start, throttle, debounce, _off, _state, _on, _dispose } from './core.js';
2
+ import { batch, computed, effect, signal, untracked } from './core.js';
3
+ import * as signals from './signal.js';
4
+ import sprae, { use, decorate, directive, modifier, parse, throttle, debounce, _off, _state, _on, _dispose, _add, call } from './core.js';
5
+ import pkg from './package.json' with { type: 'json' };
4
6
 
5
7
  import _if from "./directive/if.js";
6
8
  import _else from "./directive/else.js";
@@ -12,71 +14,64 @@ import _value from "./directive/value.js";
12
14
  import _ref from "./directive/ref.js";
13
15
  import _scope from "./directive/scope.js";
14
16
  import _each from "./directive/each.js";
15
- import _default from "./directive/default.js";
17
+ import _default from "./directive/_.js";
16
18
  import _spread from "./directive/spread.js";
19
+ import _event from "./directive/event.js";
20
+ import _seq from "./directive/sequence.js";
17
21
 
18
22
 
19
23
  Object.assign(directive, {
20
- // :x="x"
21
- '*': _default,
22
-
23
- // FIXME
24
- // 'on*': _on,
25
-
26
- // :="{a,b,c}"
24
+ _: (el, state, expr, name) => (name.startsWith('on') ? _event : _default)(el, state, expr, name),
27
25
  '': _spread,
28
-
29
- // :class="[a, b, c]"
30
26
  class: _class,
31
-
32
- // :text="..."
33
27
  text: _text,
34
-
35
- // :style="..."
36
28
  style: _style,
37
-
38
- // :fx="..."
39
29
  fx: _fx,
40
-
41
- // :value - 2 way binding like x-model
42
30
  value: _value,
43
-
44
- // :ref="..."
45
31
  ref: _ref,
46
-
47
- // :scope creates variables scope for a subtree
48
32
  scope: _scope,
49
-
50
33
  if: _if,
51
34
  else: _else,
52
-
53
- // :each="v,k in src"
54
35
  each: _each
55
36
  })
56
37
 
57
- Object.assign(modifier, {
58
- debounce: (fn,
59
- _how = 250,
60
- _schedule = _how === "tick" ? queueMicrotask : _how === "raf" ? requestAnimationFrame : _how === "idle" ? requestIdleCallback : ((fn) => setTimeout(fn, _how)),
61
- _count = 0
62
- ) =>
63
- debounce(fn, _schedule),
64
-
65
- throttle: (fn, _how = 250, _schedule = _how === "tick" ? queueMicrotask : _how === "raf" ? requestAnimationFrame : ((fn) => setTimeout(fn, _how))) => (
66
- throttle(fn, _schedule)
67
- ),
68
38
 
69
- once: (fn, _done, _fn) => Object.assign((e) => !_done && (_done = 1, fn(e)), { once: true }),
39
+ /**
40
+ * Directive initializer (with modifiers support)
41
+ * @type {(el: HTMLElement, name:string, value:string, state:Object) => Function}
42
+ * */
43
+ const dir = (target, name, expr, state) => {
44
+ let [dirName, ...mods] = name.split('.'), create = directive[dirName] || directive._
70
45
 
71
- // event modifiers
72
- // actions
73
- prevent: (fn) => (e) => (e?.preventDefault(), fn(e)),
74
- stop: (fn) => (e) => (e?.stopPropagation(), fn(e)),
75
- immediate: (fn) => (e) => (e?.stopImmediatePropagation(), fn(e)),
46
+ return () => {
47
+ let update = create(target, state, expr, name)
76
48
 
77
- // options
78
- passive: fn => (fn.passive = true, fn),
79
- capture: fn => (fn.capture = true, fn),
49
+ if (!update?.call) return update?.[_dispose]
50
+
51
+ // throttle prevents multiple updates within one tick as well as isolates stack for each update
52
+ let trigger = decorate(Object.assign(throttle(() => change.value++), { target }), mods),
53
+ change = signal(0), // signal authorized to trigger effect: 0 = init; >0 = trigger
54
+ count = 0, // called effect count
55
+ evaluate = update.eval ?? parse(expr).bind(target),
56
+ _out, out = () => (_out && call(_out), _out=null) // effect trigger and invoke may happen in the same tick, so it will be effect-within-effect call - we need to store output of evaluate to return from trigger effect
57
+
58
+ state = target[_state] ?? state
59
+
60
+ return effect(() => (
61
+ // if planned count is same as actual count - plan new update, else update right away
62
+ change.value == count ? (trigger()) : (count = change.value, _out = evaluate(state, update)),
63
+ out
64
+ ))
65
+ }
66
+ }
67
+
68
+ Object.assign(modifier, {
69
+ // timing
70
+ debounce: (fn, _how = 250) => debounce(fn, (_how ||= 0, (fn) => setTimeout(fn, _how))),
71
+ throttle: (fn, _how = 250) => throttle(fn, (_how ||= 0, (fn) => setTimeout(fn, _how))),
72
+ tick: (fn) => (e) => (queueMicrotask(() => fn(e))),
73
+ raf: (fn) => (e) => requestAnimationFrame(() => fn(e)),
74
+ once: (fn, _done, _fn) => (_fn = (e) => !_done && (_done = 1, fn(e)), _fn.once = true, _fn),
80
75
 
81
76
  // target
82
77
  window: fn => (fn.target = fn.target.ownerDocument.defaultView, fn),
@@ -84,14 +79,18 @@ Object.assign(modifier, {
84
79
  root: fn => (fn.target = fn.target.ownerDocument.documentElement, fn),
85
80
  body: fn => (fn.target = fn.target.ownerDocument.body, fn),
86
81
  parent: fn => (fn.target = fn.target.parentNode, fn),
87
-
88
- // testers
89
82
  self: (fn) => (e) => (e.target === fn.target && fn(e)),
90
-
91
83
  outside: (fn) => (e, _target) => (
92
84
  _target = fn.target,
93
85
  !_target.contains(e.target) && e.target.isConnected && (_target.offsetWidth || _target.offsetHeight)
94
86
  ),
87
+
88
+ // events
89
+ prevent: (fn) => (e) => (e?.preventDefault(), fn(e)),
90
+ stop: (fn) => (e) => (e?.stopPropagation(), fn(e)),
91
+ immediate: (fn) => (e) => (e?.stopImmediatePropagation(), fn(e)),
92
+ passive: fn => (fn.passive = true, fn),
93
+ capture: fn => (fn.capture = true, fn),
95
94
  })
96
95
 
97
96
  // key testers
@@ -99,7 +98,8 @@ const keys = {
99
98
  ctrl: e => e.ctrlKey || e.key === "Control" || e.key === "Ctrl",
100
99
  shift: e => e.shiftKey || e.key === "Shift",
101
100
  alt: e => e.altKey || e.key === "Alt",
102
- meta: e => e.metaKey || e.key === "Meta" || e.key === "Command",
101
+ meta: e => e.metaKey || e.key === "Meta",
102
+ cmd: e => e.metaKey || e.key === "Command",
103
103
  arrow: e => e.key.startsWith("Arrow"),
104
104
  enter: e => e.key === "Enter",
105
105
  esc: e => e.key.startsWith("Esc"),
@@ -112,23 +112,57 @@ const keys = {
112
112
  };
113
113
 
114
114
  // augment modifiers with key testers
115
- for (let k in keys) modifier[k] = (fn, ...params) => (e) => keys[k](e) && params.every(k => keys[k]?.(e) ?? e.key === k) && fn(e)
115
+ for (let k in keys) modifier[k] = (fn, a, b) => (e) => keys[k](e) && (!a || keys[a]?.(e)) && (!b || keys[b]?.(e)) && fn(e)
116
+
116
117
 
117
118
  use({
118
- compile: expr => {
119
- return sprae.constructor(`with (arguments[0]) { ${expr} }`)
119
+ compile: expr => sprae.constructor(`with (arguments[0]) { ${expr} }`),
120
+ dir: (el, name, expr, state) => {
121
+ // sequences shortcut
122
+ if (name.includes('..')) return () => _seq(el, state, expr, name)[_dispose]
123
+ return name.split(':').reduce((prev, str) => {
124
+ let start = dir(el, str, expr, state)
125
+ return !prev ? start : (p, s) => (p = prev(), s = start(), () => { p(); s() })
126
+ }, null)
120
127
  },
121
-
122
- // signals
123
- signal, effect, computed, batch, untracked
128
+ ...signals
124
129
  })
125
130
 
131
+
126
132
  // expose for runtime config
127
133
  sprae.use = use
128
134
  sprae.store = store
129
135
  sprae.directive = directive
130
136
  sprae.modifier = modifier
131
- sprae.start = start
137
+ sprae.version = pkg.version;
138
+
139
+
140
+ /**
141
+ * Lifecycle hanger: spraes automatically any new nodes
142
+ */
143
+ const start = sprae.start = (root = document.body, values) => {
144
+ const state = store(values)
145
+ sprae(root, state);
146
+ const mo = new MutationObserver(mutations => {
147
+ for (const m of mutations) {
148
+ for (const el of m.addedNodes) {
149
+ // el can be spraed or removed by subsprae (like within :each/:if)
150
+ if (el.nodeType === 1 && el[_state] === undefined && root.contains(el)) {
151
+ // even if element has no spraeable attrs, some of its children can have
152
+ root[_add](el)
153
+ // sprae(el, state, root);
154
+ }
155
+ }
156
+ // for (const el of m.removedNodes) el[Symbol.dispose]?.()
157
+ }
158
+ });
159
+ mo.observe(root, { childList: true, subtree: true });
160
+ return state
161
+ }
162
+
163
+
164
+ // version placeholder for bundler
165
+ sprae.version = "[VI]{{inject}}[/VI]"
132
166
 
133
167
  export default sprae
134
168
  export { sprae, store, signal, effect, computed, batch, untracked, start, use }
package/store.js CHANGED
@@ -43,7 +43,7 @@ export const store = (values, parent) => {
43
43
  },
44
44
 
45
45
  set: (_, k, v, _s) => {
46
- // console.group('SET', k, v, signals, k in signals)
46
+ // console.group('SET', k, v)
47
47
  if (k in signals) return set(signals, k, v), 1
48
48
 
49
49
  // turn off sandbox to check if parents have the prop - we don't want to create new prop in global scope
@@ -1,3 +0,0 @@
1
- import { attr, call } from "../core.js";
2
-
3
- export default (el, st, ex, name) => v => attr(el, name, call(v, el.getAttribute(name)))