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/signal.js CHANGED
@@ -1,53 +1,54 @@
1
- // ulive copy, stable minimal implementation
2
- let current;
1
+ // preact-signals minimal implementation
2
+ let current, depth = 0, batched;
3
3
 
4
- export let signal = (v, s, obs = new Set) => (
5
- s = {
4
+ // default signals impl
5
+
6
+ export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
7
+ _s = {
6
8
  get value() {
7
- current?.deps.push(obs.add(current));
9
+ current?.deps.push(_obs.add(current));
8
10
  return v
9
11
  },
10
12
  set value(val) {
11
13
  if (val === v) return
12
14
  v = val;
13
- for (let sub of obs) sub(); // notify effects
15
+ for (let sub of _obs) batched ? batched.add(sub) : sub(); // notify effects
14
16
  },
15
17
  peek() { return v },
18
+ toJSON: _v, then: _v, toString: _v, valueOf: _v
19
+ }
20
+ )
21
+
22
+ export const effect = (fn, _teardown, _fx, _deps, __tmp) => (
23
+ _fx = (prev) => {
24
+ __tmp = _teardown;
25
+ _teardown = null; // we null _teardown to avoid repeated call in case of recursive update
26
+ __tmp?.call?.();
27
+ prev = current, current = _fx
28
+ if (depth++ > 10) throw 'Cycle detected';
29
+ try { _teardown = fn(); } finally { current = prev; depth-- }
16
30
  },
17
- s.toJSON = s.then = s.toString = s.valueOf = () => s.value,
18
- s
19
- ),
20
- effect = (fn, teardown, fx, deps) => (
21
- fx = (prev) => {
22
- teardown?.call?.();
23
- prev = current, current = fx;
24
- try { teardown = fn(); } finally { current = prev; }
25
- },
26
- deps = fx.deps = [],
31
+ _deps = _fx.deps = [],
27
32
 
28
- fx(),
29
- (dep) => { teardown?.call?.(); while (dep = deps.pop()) dep.delete(fx); }
30
- ),
31
- computed = (fn, s = signal(), c, e) => (
32
- c = {
33
- get value() {
34
- e ||= effect(() => s.value = fn());
35
- return s.value
36
- },
37
- peek: s.peek
33
+ _fx(),
34
+ (dep) => { _teardown?.call?.(); while (dep = _deps.pop()) dep.delete(_fx); }
35
+ )
36
+
37
+ export const computed = (fn, _s = signal(), _c, _e, _v = () => _c.value) => (
38
+ _c = {
39
+ get value() {
40
+ _e ||= effect(() => _s.value = fn());
41
+ return _s.value
38
42
  },
39
- c.toJSON = c.then = c.toString = c.valueOf = () => c.value,
40
- c
41
- ),
42
- batch = fn => fn(),
43
- untracked = batch,
44
- // untracked = (fn, prev, v) => (prev = current, current = null, v = fn(), current = prev, v),
43
+ peek: _s.peek,
44
+ toJSON: _v, then: _v, toString: _v, valueOf: _v
45
+ }
46
+ )
47
+
48
+ export const batch = (fn, _first = !batched) => {
49
+ batched ??= new Set;
50
+ try { fn(); }
51
+ finally { if (_first) { for (const fx of batched) fx(); batched = null } }
52
+ }
45
53
 
46
- // signals adapter - allows switching signals implementation and not depend on core
47
- use = (s) => (
48
- signal = s.signal,
49
- effect = s.effect,
50
- computed = s.computed,
51
- batch = s.batch || batch,
52
- untracked = s.untracked || untracked
53
- )
54
+ export const untracked = (fn, _prev, _v) => (_prev = current, current = null, _v = fn(), current = _prev, _v)
package/sprae.js CHANGED
@@ -1,20 +1,129 @@
1
- import sprae from './core.js'
2
-
3
- // default directives
4
- import './directive/if.js'
5
- import './directive/each.js'
6
- import './directive/ref.js'
7
- import './directive/with.js'
8
- import './directive/text.js'
9
- import './directive/class.js'
10
- import './directive/style.js'
11
- import './directive/value.js'
12
- import './directive/fx.js'
13
- import './directive/default.js'
14
- import './directive/aria.js'
15
- import './directive/data.js'
16
-
17
- // default compiler (indirect new Function to avoid detector)
18
- sprae.use({ compile: expr => sprae.constructor(`with (arguments[0]) { return ${expr} };`) })
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, _add, _off, _state, _on, _dispose } from './core.js';
4
+
5
+ import _if from "./directive/if.js";
6
+ import _else from "./directive/else.js";
7
+ import _text from "./directive/text.js";
8
+ import _class from "./directive/class.js";
9
+ import _style from "./directive/style.js";
10
+ import _fx from "./directive/fx.js";
11
+ import _value from "./directive/value.js";
12
+ import _ref from "./directive/ref.js";
13
+ import _scope from "./directive/scope.js";
14
+ import _each from "./directive/each.js";
15
+ import _default from "./directive/default.js";
16
+ import _spread from "./directive/spread.js";
17
+
18
+
19
+ Object.assign(directive, {
20
+ // :x="x"
21
+ '*': _default,
22
+
23
+ // :="{a,b,c}"
24
+ '': _spread,
25
+
26
+ // :class="[a, b, c]"
27
+ class: _class,
28
+
29
+ // :text="..."
30
+ text: _text,
31
+
32
+ // :style="..."
33
+ style: _style,
34
+
35
+ // :fx="..."
36
+ fx: _fx,
37
+
38
+ // :value - 2 way binding like x-model
39
+ value: _value,
40
+
41
+ // :ref="..."
42
+ ref: _ref,
43
+
44
+ // :scope creates variables scope for a subtree
45
+ scope: _scope,
46
+
47
+ if: _if,
48
+ else: _else,
49
+
50
+ // :each="v,k in src"
51
+ each: _each
52
+ })
53
+
54
+ Object.assign(modifier, {
55
+ debounce: (fn,
56
+ _how = 250,
57
+ _schedule = _how === "tick" ? queueMicrotask : _how === "raf" ? requestAnimationFrame : _how === "idle" ? requestIdleCallback : ((fn) => setTimeout(fn, _how)),
58
+ _count = 0
59
+ ) =>
60
+ debounce(fn, _schedule),
61
+
62
+ throttle: (fn, _how = 250, _schedule = _how === "tick" ? queueMicrotask : _how === "raf" ? requestAnimationFrame : ((fn) => setTimeout(fn, _how))) => (
63
+ throttle(fn, _schedule)
64
+ ),
65
+
66
+ once: (fn, _done, _fn) => Object.assign((e) => !_done && (_done = 1, fn(e)), { once: true }),
67
+
68
+ // event modifiers
69
+ // actions
70
+ prevent: (fn) => (e) => (e?.preventDefault(), fn(e)),
71
+ stop: (fn) => (e) => (e?.stopPropagation(), fn(e)),
72
+ immediate: (fn) => (e) => (e?.stopImmediatePropagation(), fn(e)),
73
+
74
+ // options
75
+ passive: fn => (fn.passive = true, fn),
76
+ capture: fn => (fn.capture = true, fn),
77
+
78
+ // target
79
+ window: fn => (fn.target = window, fn),
80
+ document: fn => (fn.target = document, fn),
81
+ parent: fn => (fn.target = fn.target.parentNode, fn),
82
+
83
+ // test
84
+ self: (fn) => (e) => (e.target === fn.target && fn(e)),
85
+
86
+ outside: (fn) => (e, _target) => (
87
+ _target = fn.target,
88
+ !_target.contains(e.target) && e.target.isConnected && (_target.offsetWidth || _target.offsetHeight)
89
+ ),
90
+ })
91
+
92
+ // key testers
93
+ const keys = {
94
+ ctrl: e => e.ctrlKey || e.key === "Control" || e.key === "Ctrl",
95
+ shift: e => e.shiftKey || e.key === "Shift",
96
+ alt: e => e.altKey || e.key === "Alt",
97
+ meta: e => e.metaKey || e.key === "Meta" || e.key === "Command",
98
+ arrow: e => e.key.startsWith("Arrow"),
99
+ enter: e => e.key === "Enter",
100
+ esc: e => e.key.startsWith("Esc"),
101
+ tab: e => e.key === "Tab",
102
+ space: e => e.key === " " || e.key === "Space" || e.key === " ",
103
+ delete: e => e.key === "Delete" || e.key === "Backspace",
104
+ digit: e => /^\d$/.test(e.key),
105
+ letter: e => /^\p{L}$/gu.test(e.key),
106
+ char: e => /^\S$/.test(e.key),
107
+ };
108
+
109
+ // augment modifiers with key testers
110
+ for (let k in keys) modifier[k] = (fn, ...params) => (e) => keys[k](e) && params.every(k => keys[k]?.(e) ?? e.key === k) && fn(e)
111
+
112
+ use({
113
+ compile: expr => {
114
+ return sprae.constructor(`with (arguments[0]) { ${expr} }`)
115
+ },
116
+
117
+ // signals
118
+ signal, effect, computed, batch, untracked
119
+ })
120
+
121
+ // expose for runtime config
122
+ sprae.use = use
123
+ sprae.store = store
124
+ sprae.directive = directive
125
+ sprae.modifier = modifier
126
+ sprae.start = start
19
127
 
20
128
  export default sprae
129
+ export { sprae, store, signal, effect, computed, batch, untracked, start, use }
package/store.js CHANGED
@@ -1,139 +1,152 @@
1
1
  // signals-based proxy
2
- import { signal, computed, batch } from './signal.js'
3
- import { parse } from './core.js';
2
+ import { signal, computed, batch, untracked } from './core.js'
4
3
 
4
+
5
+ // _signals allows both storing signals and checking instance, which would be difficult with WeakMap
5
6
  export const _signals = Symbol('signals'),
6
- _change = Symbol('change'),
7
- _stash = '__',
8
-
9
- // object store is not lazy
10
- store = (values, parent) => {
11
- if (!values) return values
12
-
13
- // ignore existing state as argument or globals
14
- if (values[_signals] || values[Symbol.toStringTag]) return values;
15
-
16
- // non-objects: for array redirect to list
17
- if (values.constructor !== Object) return Array.isArray(values) ? list(values) : values
18
-
19
- // we must inherit signals to allow dynamic extend of parent state
20
- let signals = Object.create(parent?.[_signals] || {}),
21
- _len = signal(Object.keys(values).length),
22
- stash
23
-
24
- // proxy conducts prop access to signals
25
- let state = new Proxy(signals, {
26
- get: (_, k) => k === _change ? _len : k === _signals ? signals : k === _stash ? stash : k in signals ? signals[k]?.valueOf() : globalThis[k],
27
- set: (_, k, v, s) => k === _stash ? (stash = v, 1) : (s = k in signals, set(signals, k, v), s || ++_len.value), // bump length for new signal
28
- deleteProperty: (_, k) => (signals[k] && (signals[k][Symbol.dispose]?.(), delete signals[k], _len.value--), 1),
29
- // subscribe to length when object is spread
30
- ownKeys: () => (_len.value, Reflect.ownKeys(signals)),
31
- has: _ => true // sandbox prevents writing to global
32
- }),
7
+ _change = Symbol('change'),
8
+ _set = Symbol('set')
9
+
10
+ // object store is not lazy
11
+ // parent defines parent scope or sandbox
12
+ export const store = (values, parent = globalThis) => {
13
+ if (!values) return values
14
+
15
+ // ignore globals
16
+ if (values[Symbol.toStringTag]) return values;
17
+
18
+ // bypass existing store
19
+ if (values[_signals]) return values
20
+
21
+ // non-objects: for array redirect to list
22
+ if (values.constructor !== Object) return Array.isArray(values) ? list(values) : values
23
+
24
+ // _change stores total number of keys to track new props
25
+ // NOTE: be careful
26
+ let keyCount = Object.keys(values).length,
27
+ signals = { }
28
+
29
+ // proxy conducts prop access to signals
30
+ let state = new Proxy(Object.assign(signals, {[_change]: signal(keyCount), [_signals]: signals}), {
31
+ get: (_, k) => (k in signals ? (signals[k] ? signals[k].valueOf() : signals[k]) : parent[k]),
32
+ set: (_, k, v, _s) => (k in signals ? set(signals, k, v) : (create(signals, k, v), signals[_change].value = ++keyCount), 1), // bump length for new signal
33
+ // FIXME: try to avild calling Symbol.dispose here
34
+ deleteProperty: (_, k) => (k in signals && (k[0] != '_' && signals[k]?.[Symbol.dispose]?.(), delete signals[k], signals[_change].value = --keyCount), 1),
35
+ // subscribe to length when object is spread
36
+ ownKeys: () => (signals[_change].value, Reflect.ownKeys(signals)),
37
+ has: _ => 1 // sandbox prevents writing to global
38
+ }),
39
+
40
+ // init signals for values
41
+ descs = Object.getOwnPropertyDescriptors(values)
42
+
43
+ for (let k in values) {
44
+ // getter turns into computed
45
+ if (descs[k]?.get)
46
+ // stash setter
47
+ (signals[k] = computed(descs[k].get.bind(state)))[_set] = descs[k].set?.bind(state);
48
+
49
+ // init blank signal - make sure we don't take prototype one
50
+ else create(signals, k, values[k])
51
+ }
33
52
 
34
- // init signals for values
35
- descs = Object.getOwnPropertyDescriptors(values)
53
+ return state
54
+ }
36
55
 
37
- for (let k in values) {
38
- // getter turns into computed
39
- if (descs[k]?.get)
40
- // stash setter
41
- (signals[k] = computed(descs[k].get.bind(state)))._set = descs[k].set?.bind(state);
56
+ const mut = ['push', 'pop', 'shift', 'unshift', 'splice']
42
57
 
43
- else
44
- // init blank signal - make sure we don't take prototype one
45
- signals[k] = null, set(signals, k, values[k]);
46
- }
58
+ // array store - signals are lazy since arrays can be very large & expensive
59
+ const list = (values, parent = globalThis) => {
47
60
 
48
- return state
49
- },
61
+ // gotta fill with null since proto methods like .reduce may fail
62
+ let signals = Array(values.length).fill(null),
50
63
 
51
- // array store - signals are lazy since arrays can be very large & expensive
52
- list = values => {
53
- // track last accessed property to find out if .length was directly accessed from expression or via .push/etc method
54
- let lastProp,
64
+ // if .length was accessed from mutator (.push/etc) method
65
+ isMut = false,
55
66
 
56
- // .length signal is stored separately, since it cannot be replaced on array
57
- _len = signal(values.length),
67
+ // since array mutator methods read .length internally only once, we disable it on the moment of call, allowing rest of operations to be reactive
68
+ mut = fn => function () {isMut = true; return fn.apply(this, arguments); },
58
69
 
59
- // gotta fill with null since proto methods like .reduce may fail
60
- signals = Array(values.length).fill(),
70
+ length = signal(values.length),
61
71
 
62
- // proxy conducts prop access to signals
63
- state = new Proxy(signals, {
72
+ // proxy conducts prop access to signals
73
+ state = new Proxy(
74
+ Object.assign(signals, {
75
+ [_change]: length,
76
+ [_signals]: signals,
77
+ push: mut(signals.push),
78
+ pop: mut(signals.pop),
79
+ shift: mut(signals.shift),
80
+ unshift: mut(signals.unshift),
81
+ splice: mut(signals.splice),
82
+ }),
83
+ {
64
84
  get(_, k) {
65
- // covers Symbol.isConcatSpreadable etc.
66
- if (typeof k === 'symbol') return k === _change ? _len : k === _signals ? signals : signals[k]
85
+ // console.log('GET', k, isMut)
67
86
 
68
- // if .length is read within .push/etc - peek signal to avoid recursive subscription
69
- if (k === 'length') return mut.includes(lastProp) ? _len.peek() : _len.value;
87
+ // if .length is read within mutators - peek signal to avoid recursive subscription
88
+ // we need to ignore it only once and keep for the rest of the mutator call
89
+ if (k === 'length') return isMut ? (isMut = false, signals.length) : length.value;
70
90
 
71
- lastProp = k;
91
+ // non-numeric
92
+ if (typeof k === 'symbol' || isNaN(k)) return signals[k]?.valueOf() ?? parent[k];
72
93
 
73
94
  // create signal (lazy)
74
95
  // NOTE: if you decide to unlazy values, think about large arrays - init upfront can be costly
75
- return (signals[k] ?? (signals[k] = signal(store(values[k])))).valueOf()
96
+ return (signals[k] ??= signal(store(values[k]))).valueOf()
76
97
  },
77
98
 
78
99
  set(_, k, v) {
100
+ // console.log('SET', k, v)
101
+
79
102
  // .length
80
103
  if (k === 'length') {
81
104
  // force cleaning up tail
82
105
  for (let i = v; i < signals.length; i++) delete state[i]
83
106
  // .length = N directly
84
- _len.value = signals.length = v;
107
+ length.value = signals.length = v;
85
108
  }
86
- else {
87
- set(signals, k, v)
88
109
 
89
- // force changing length, if eg. a=[]; a[1]=1 - need to come after setting the item
90
- if (k >= _len.peek()) _len.value = signals.length = +k + 1
91
- }
110
+ // force changing length, if eg. a=[]; a[1]=1 - need to come after setting the item
111
+ else if (k >= signals.length) create(signals, k, v), state.length = +k + 1
112
+
113
+ // existing signal
114
+ else signals[k] ? set(signals, k, v) : create(signals, k, v)
92
115
 
93
116
  return 1
94
117
  },
95
118
 
119
+ // dispose notifies any signal deps, like :each
96
120
  deleteProperty: (_, k) => (signals[k]?.[Symbol.dispose]?.(), delete signals[k], 1),
97
121
  })
98
122
 
99
- return state
100
- }
123
+ return state
124
+ }
101
125
 
102
- // length changing methods
103
- const mut = ['push', 'pop', 'shift', 'unshift', 'splice']
126
+ // create signal value, skip untracked
127
+ const create = (signals, k, v) => (signals[k] = k[0] == '_' || v?.peek ? v : signal(store(v)))
104
128
 
105
129
  // set/update signal value
106
- const set = (signals, k, v) => {
107
- let s = signals[k], cur
108
-
109
- // untracked
110
- if (k[0] === '_') signals[k] = v
111
- // new property. preserve signal value as is
112
- else if (!s) signals[k] = s = v?.peek ? v : signal(store(v))
130
+ const set = (signals, k, v, _s, _v) => {
113
131
  // skip unchanged (although can be handled by last condition - we skip a few checks this way)
114
- else if (v === (cur = s.peek()));
115
- // stashed _set for value with getter/setter
116
- else if (s._set) s._set(v)
117
- // patch array
118
- else if (Array.isArray(v) && Array.isArray(cur)) {
119
- // if we update plain array (stored in signal) - take over value instead
120
- if (cur[_change]) batch(() => {
121
- for (let i = 0; i < v.length; i++) cur[i] = v[i]
122
- cur.length = v.length // forces deleting tail signals
123
- })
124
- else s.value = v
125
- }
126
- // .x = y
127
- else s.value = store(v)
132
+ return k[0] === '_' ? (signals[k] = v) :
133
+ (v !== (_v = (_s = signals[k]).peek())) && (
134
+ // stashed _set for value with getter/setter
135
+ _s[_set] ? _s[_set](v) :
136
+ // patch array
137
+ Array.isArray(v) && Array.isArray(_v) ?
138
+ // if we update plain array (stored in signal) - take over value instead
139
+ // since input value can be store, we have to make sure we don't subscribe to its length or values
140
+ // FIXME: generalize to objects
141
+ _change in _v ? untracked(() => batch(() => {
142
+ for (let i = 0; i < v.length; i++) _v[i] = v[i]
143
+ _v.length = v.length // forces deleting tail signals
144
+ })) : _s.value = v :
145
+ // .x = y
146
+ (_s.value = store(v))
147
+ )
128
148
  }
129
149
 
130
- // create expression setter, reflecting value back to state
131
- export const setter = (expr, set = parse(`${expr}=${_stash}`)) => (
132
- (state, value) => (
133
- state[_stash] = value, // save value to stash
134
- set(state)
135
- )
136
- )
137
150
 
138
151
  // make sure state contains first element of path, eg. `a` from `a.b[c]`
139
152
  // NOTE: we don't need since we force proxy sandbox
package/directive/aria.js DELETED
@@ -1,6 +0,0 @@
1
- import { dir } from "../core.js";
2
- import { attr, dashcase } from './default.js'
3
-
4
- dir('aria', (el) => value => {
5
- for (let key in value) attr(el, 'aria-' + dashcase(key), value[key] == null ? null : value[key] + '')
6
- })
package/directive/data.js DELETED
@@ -1,3 +0,0 @@
1
- import { dir } from "../core.js";
2
-
3
- dir('data', el => value => {for (let key in value) el.dataset[key] = value[key];})
package/directive/with.js DELETED
@@ -1,12 +0,0 @@
1
- import sprae, { dir } from "../core.js";
2
- import { untracked } from "../signal.js";
3
- import store, { _signals } from '../store.js';
4
-
5
- dir('with', (el, rootState, state) => (
6
- state=null,
7
- values => !state ?
8
- // NOTE: we force untracked because internal directives can eval outside of effects (like ref etc) that would cause unwanted subscribe
9
- // FIXME: since this can be async effect, we should create & sprae it in advance.
10
- untracked(() => sprae(el, state = store(values, rootState))) :
11
- sprae(el, values)
12
- ))