sprae 9.1.1 → 10.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 CHANGED
@@ -1,97 +1,93 @@
1
+ import { effect, untracked, use } from "./signal.js";
2
+ import store, { _signals } from './store.js';
3
+
1
4
  // polyfill
2
5
  const _dispose = (Symbol.dispose ||= Symbol("dispose"));
3
6
 
4
7
  // mark
5
8
  const SPRAE = `∴`
6
9
 
7
- // signals impl
8
- export let signal, effect, batch, computed, untracked
9
-
10
10
  // reserved directives - order matters!
11
11
  export const directive = {};
12
12
 
13
13
  // sprae element: apply directives
14
14
  const memo = new WeakMap();
15
15
 
16
- export default function sprae(container, values) {
17
- if (!container.children) return // text nodes, comments etc
18
-
19
- // repeated call can be caused by :each with new objects with old keys needs an update
20
- if (memo.has(container)) {
21
- const [state, effects] = memo.get(container)
22
- // we rewrite signals instead of update, because user should have what he provided
23
- for (let k in values) { state[k] = values[k] }
24
- // since we call direct updates here, we have to make sure
25
- // we don't subscribe outer effect, as in case of :each
26
- untracked(() => { for (let fx of effects) fx() })
27
- }
16
+ export default function sprae(els, values) {
17
+ let state
28
18
 
29
- // take over existing state instead of creating clone
30
- const state = values || {};
31
- const effects = [];
32
-
33
- // init directives on element
34
- const init = (el, parent = el.parentNode) => {
35
- if (el.attributes) {
36
- // init generic-name attributes second
37
- for (let i = 0; i < el.attributes.length;) {
38
- let attr = el.attributes[i];
39
-
40
- if (attr.name[0] === ':') {
41
- el.removeAttribute(attr.name);
42
-
43
- // multiple attributes like :id:for=""
44
- let names = attr.name.slice(1).split(':')
45
-
46
- // NOTE: secondary directives don't stop flow nor extend state, so no need to check
47
- for (let name of names) {
48
- let dir = directive[name] || directive.default
49
- let evaluate = (dir.parse || parse)(attr.value, parse)
50
- let update = dir(el, evaluate, state, name);
51
- if (update) {
52
- update[_dispose] = effect(update);
53
- effects.push(update);
54
- }
55
- }
56
-
57
- // stop if element was spraed by directive or skipped (detached) like in case of :if or :each
58
- if (memo.has(el)) return;
59
- if (el.parentNode !== parent) return false;
60
- } else i++;
19
+ // make multiple items
20
+ if (!els?.[Symbol.iterator]) els = [els]
21
+
22
+ for (let el of els) {
23
+ // text nodes, comments etc - but collections are fine
24
+ if (el?.children) {
25
+ // repeated call can be caused by :each with new objects with old keys needs an update
26
+ if (memo.has(el)) {
27
+ // we rewrite signals instead of update, because user should have what he provided
28
+ Object.assign(memo.get(el), values)
61
29
  }
62
- }
30
+ else {
31
+ // take over existing state instead of creating clone
32
+ state ||= store(values || {});
63
33
 
64
- for (let i = 0, child; child = el.children[i]; i++) {
65
- // if element was removed from parent (skipped) - reduce index
66
- if (init(child, el) === false) i--;
34
+ init(el, state);
35
+
36
+ // if element was spraed by :with or :each instruction - skip, otherwise save
37
+ if (!memo.has(el)) memo.set(el, state);
38
+ }
67
39
  }
68
40
  };
69
41
 
70
- init(container);
42
+ return state;
43
+ }
71
44
 
72
- // if element was spraed by :scope or :each instruction - skip
73
- if (memo.has(container)) return state// memo.get(container)
45
+ // init directives on a single element
46
+ function init(el, state, parent = el.parentNode, effects = []) {
47
+ if (el.attributes) {
48
+ // init generic-name attributes second
49
+ for (let i = 0; i < el.attributes.length;) {
50
+ let attr = el.attributes[i];
51
+
52
+ if (attr.name[0] === ':') {
53
+ el.removeAttribute(attr.name);
54
+
55
+ // multiple attributes like :id:for=""
56
+ let names = attr.name.slice(1).split(':')
57
+
58
+ // NOTE: secondary directives don't stop flow nor extend state, so no need to check
59
+ for (let name of names) {
60
+ let dir = directive[name] || directive.default
61
+ let evaluate = (dir.parse || parse)(attr.value, parse)
62
+ let dispose = dir(el, evaluate, state, name);
63
+ if (dispose) effects.push(dispose);
64
+ }
65
+
66
+ // stop if element was spraed by internal directive
67
+ if (memo.has(el)) return;
68
+
69
+ // stop if element is skipped (detached) like in case of :if or :each
70
+ if (el.parentNode !== parent) return;
71
+ } else i++;
72
+ }
73
+ }
74
74
 
75
- // save
76
- memo.set(container, [state, effects]);
77
- container.classList?.add(SPRAE); // mark spraed element
75
+ for (let child of [...el.children]) init(child, state, el)
78
76
 
79
- // expose dispose
80
- container[_dispose] = () => {
81
- while (effects.length) effects.pop()[_dispose]();
82
- container.classList.remove(SPRAE)
83
- memo.delete(container);
77
+ // mark spraed element
78
+ el.classList?.add(SPRAE);
79
+ el[_dispose] = () => {
80
+ while (effects.length) effects.pop()();
81
+ el.classList.remove(SPRAE);
82
+ memo.delete(el);
84
83
  // NOTE: each child disposes own children etc.
85
- let els = container.getElementsByClassName(SPRAE);
84
+ let els = el.getElementsByClassName(SPRAE);
86
85
  while (els.length) els[0][_dispose]?.()
87
86
  }
87
+ };
88
88
 
89
- return state;
90
- }
91
-
92
- // default compiler
89
+ // compiler
93
90
  const evalMemo = {};
94
-
95
91
  const parse = (expr, dir, fn) => {
96
92
  if (fn = evalMemo[expr = expr.trim()]) return fn
97
93
 
@@ -99,28 +95,15 @@ const parse = (expr, dir, fn) => {
99
95
  try { fn = compile(expr); }
100
96
  catch (e) { throw Object.assign(e, { message: `∴ ${e.message}\n\n${dir}${expr ? `="${expr}"\n\n` : ""}`, expr }) }
101
97
 
102
- fn.expr = expr
103
-
104
98
  // runtime errors
105
99
  return evalMemo[expr] = fn
106
100
  }
107
101
 
108
- // compiler
109
102
  export let compile
110
103
 
111
- // DOM swapper
112
- export let swap
113
-
114
104
  // configure signals/compile/differ
115
105
  // it's more compact than using sprae.signal = signal etc.
116
106
  sprae.use = s => {
117
- s.signal && (
118
- signal = s.signal,
119
- effect = s.effect,
120
- computed = s.computed,
121
- batch = s.batch || (fn => fn()),
122
- untracked = s.untracked || batch
123
- );
124
- s.swap && (swap = s.swap)
125
- s.compile && (compile = s.compile)
107
+ s.signal && use(s);
108
+ s.compile && (compile = s.compile);
126
109
  }
package/directive/aria.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { directive } from "../core.js";
2
2
  import { attr, dashcase } from './default.js'
3
+ import { effect } from "../signal.js";
3
4
 
4
5
  directive['aria'] = (el, evaluate, state) => {
5
6
  const update = (value) => {
6
7
  for (let key in value) attr(el, 'aria-' + dashcase(key), value[key] == null ? null : value[key] + '');
7
8
  }
8
- return () => update(evaluate(state)?.valueOf())
9
+ return effect(() => update(evaluate(state)))
9
10
  }
@@ -1,17 +1,18 @@
1
1
  import { directive } from "../core.js";
2
2
  import { ipol } from './default.js';
3
+ import { effect } from "../signal.js";
3
4
 
4
5
  directive.class = (el, evaluate, state) => {
5
6
  let cur = new Set
6
- return () => {
7
+ return effect(() => {
7
8
  let v = evaluate(state);
8
9
  let clsx = new Set;
9
10
  if (v) {
10
- if (typeof v === "string") ipol(v?.valueOf?.(), state).split(' ').map(cls => clsx.add(cls));
11
- else if (Array.isArray(v)) v.map(v => (v = ipol(v?.valueOf?.(), state)) && clsx.add(v));
12
- else Object.entries(v).map(([k, v]) => v?.valueOf?.() && clsx.add(k));
11
+ if (typeof v === "string") ipol(v, state).split(' ').map(cls => clsx.add(cls));
12
+ else if (Array.isArray(v)) v.map(v => (v = ipol(v, state)) && clsx.add(v));
13
+ else Object.entries(v).map(([k, v]) => v && clsx.add(k));
13
14
  }
14
15
  for (let cls of cur) if (clsx.has(cls)) clsx.delete(cls); else el.classList.remove(cls);
15
16
  for (let cls of cur = clsx) el.classList.add(cls)
16
- };
17
+ });
17
18
  };
package/directive/data.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { directive } from "../core.js";
2
+ import { effect } from "../signal.js";
2
3
 
3
4
  directive['data'] = (el, evaluate, state) => {
4
- return () => {
5
- let value = evaluate(state)?.valueOf()
5
+ return effect(() => {
6
+ let value = evaluate(state)
6
7
  for (let key in value) el.dataset[key] = value[key];
7
- }
8
+ })
8
9
  }
@@ -1,22 +1,20 @@
1
1
  import { directive } from "../core.js";
2
+ import { effect } from "../signal.js";
2
3
 
3
4
  // set generic property directive
4
5
  directive.default = (el, evaluate, state, name) => {
5
- let evt = name.startsWith("on") && name.slice(2);
6
-
7
- if (evt) {
8
- let off
9
- return () => (
10
- off?.(), // intermediate teardown
11
- off = on(el, evt, evaluate(state)?.valueOf())
12
- );
13
- }
14
-
15
- return () => {
16
- let value = evaluate(state)?.valueOf();
17
- if (name) attr(el, name, ipol(value, state))
18
- else for (let key in value) attr(el, dashcase(key), ipol(value[key], state));
19
- };
6
+ let evt = name.startsWith("on") && name.slice(2), off
7
+ return effect(
8
+ evt ?
9
+ () => (
10
+ off?.(), // intermediate teardown
11
+ off = on(el, evt, evaluate(state))
12
+ ) :
13
+ () => {
14
+ let value = evaluate(state);
15
+ if (name) attr(el, name, ipol(value, state))
16
+ else for (let key in value) attr(el, dashcase(key), ipol(value[key], state));
17
+ });
20
18
  };
21
19
 
22
20
 
@@ -148,5 +146,5 @@ export const dashcase = (str) => {
148
146
 
149
147
  // interpolate a$<b> fields from context
150
148
  export const ipol = (v, state) => {
151
- return v?.replace ? v.replace(/\$<([^>]+)>/g, (match, field) => state[field]?.valueOf?.() ?? '') : v
149
+ return v?.replace ? v.replace(/\$<([^>]+)>/g, (match, field) => state[field] ?? '') : v
152
150
  };
package/directive/each.js CHANGED
@@ -1,59 +1,96 @@
1
- import sprae, { directive, swap } from "../core.js";
1
+ import sprae, { directive } from "../core.js";
2
+ import { _change, _signals } from "../store.js";
3
+ import { effect, untracked, computed } from '../signal.js';
2
4
 
3
- export const _each = Symbol(":each");
4
5
 
5
- const keys = {}, _key = Symbol('key');
6
+ export const _each = Symbol(":each");
6
7
 
7
- // :each must init before :ref, :id or any others, since it defines scope
8
- (directive.each = (tpl, [itemVar, idxVar, evaluate], state) => {
8
+ directive.each = (tpl, [itemVar, idxVar, evaluate], state) => {
9
9
  // we need :if to be able to replace holder instead of tpl for :if :each case
10
- const holder = (tpl[_each] = document.createTextNode("")), parent = tpl.parentNode;
10
+ const holder = (tpl[_each] = document.createTextNode(""));
11
11
  tpl.replaceWith(holder);
12
12
 
13
- // key -> el
14
- const elCache = new WeakMap, stateCache = new WeakMap
15
-
16
- let cur = [];
17
-
18
- const remove = el => {
19
- el.remove()
20
- el[Symbol.dispose]?.()
21
- if (el[_key]) {
22
- elCache.delete(el[_key])
23
- stateCache.delete(el[_key])
24
- }
25
- }, { insert, replace } = swap
26
-
27
- const options = { remove, insert, replace }
28
-
29
- // naive approach: whenever items change we replace full list
30
- return () => {
31
- let items = evaluate(state)?.valueOf(), els = [];
32
-
33
- if (typeof items === "number") items = Array.from({ length: items }, (_, i) => i)
34
-
35
- // let c = 0, inc = () => { if (c++ > 100) throw 'Inf recursion' }
36
- const count = new WeakMap
37
- for (let idx in items) {
38
- let el, item = items[idx], key = item?.key ?? item?.id ?? item ?? idx
39
- key = (Object(key) !== key) ? (keys[key] ||= Object(key)) : item
40
-
41
- if (key == null || count.has(key) || tpl.content) el = (tpl.content || tpl).cloneNode(true)
42
- else count.set(key, 1), (el = elCache.get(key) || (elCache.set(key, tpl.cloneNode(true)), elCache.get(key)))[_key] = key;
43
-
44
- // creating via prototype is faster in both creation time & reading time
45
- let substate = stateCache.get(key) || (stateCache.set(key, Object.create(state, { [idxVar]: { value: idx } })), stateCache.get(key));
46
- substate[itemVar] = item; // can be changed by subsequent updates, need to be writable
13
+ // we re-create items any time new items are produced
14
+ let cur, keys, prevl = 0
15
+
16
+ // separate computed effect reduces number of needed updates for the effect
17
+ const items = computed(() => {
18
+ keys = null
19
+ let items = evaluate(state)
20
+ if (typeof items === "number") items = Array.from({ length: items }, (_, i) => i + 1)
21
+ if (items?.constructor === Object) keys = Object.keys(items), items = Object.values(items)
22
+ return items || []
23
+ })
24
+
25
+ const update = () => {
26
+ // NOTE: untracked avoids rerendering full list whenever internal items or props change
27
+ untracked(() => {
28
+ let i = 0, newItems = items.value, newl = newItems.length
29
+
30
+ // plain array update, not store (signal with array) - updates full list
31
+ if (cur && !(cur[_change])) {
32
+ for (let s of cur[_signals] || []) { s[Symbol.dispose]() }
33
+ cur = null, prevl = 0
34
+ }
35
+
36
+ // delete
37
+ if (newl < prevl) {
38
+ cur.length = newl
39
+ }
40
+ // update, append, init
41
+ else {
42
+ // init
43
+ if (!cur) {
44
+ cur = newItems
45
+ }
46
+ // update
47
+ else {
48
+ for (; i < prevl; i++) {
49
+ cur[i] = newItems[i]
50
+ }
51
+ }
52
+
53
+ // append
54
+ for (; i < newl; i++) {
55
+ cur[i] = newItems[i]
56
+ let idx = i,
57
+ scope = Object.create(state, {
58
+ [itemVar]: { get() { return cur[idx] } },
59
+ [idxVar]: { value: keys ? keys[idx] : idx },
60
+ }),
61
+ el = (tpl.content || tpl).cloneNode(true), // single element or fragment
62
+ els = tpl.content ? [...el.childNodes] : [el] // total added elements
63
+
64
+ holder.before(el);
65
+ sprae(els, scope);
66
+
67
+ // signal/holder disposal removes element
68
+ ((cur[_signals] ||= [])[i] ||= {})[Symbol.dispose] = () => {
69
+ for (let el of els) el[Symbol.dispose](), el.remove()
70
+ };
71
+ }
72
+ }
73
+
74
+ prevl = newl
75
+ })
76
+ }
47
77
 
48
- sprae(el, substate);
78
+ let planned = 0
79
+ return effect(() => {
80
+ // subscribe to items change (.length)
81
+ if (!cur) items.value[_change]?.value
82
+
83
+ // make first render immediately, debounce subsequent renders
84
+ if (!planned) {
85
+ update()
86
+ queueMicrotask(() => (planned && update(), planned = 0))
87
+ } else planned++
88
+ })
89
+ }
49
90
 
50
- // document fragment
51
- if (el.nodeType === 11) els.push(...el.childNodes); else els.push(el);
52
- }
53
91
 
54
- swap(parent, cur, cur = els, holder, options);
55
- }
56
- }).parse = (expr, parse) => {
92
+ // redefine parser to exclude `[a in] b`
93
+ directive.each.parse = (expr, parse) => {
57
94
  let [leftSide, itemsExpr] = expr.split(/\s+in\s+/);
58
95
  let [itemVar, idxVar = "$"] = leftSide.split(/\s*,\s*/);
59
96
 
package/directive/fx.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { directive } from "../core.js";
2
+ import { effect } from "../signal.js";
2
3
 
3
4
  directive.fx = (el, evaluate, state) => {
4
- return () => evaluate(state);
5
+ return effect(() => evaluate(state));
5
6
  };
package/directive/if.js CHANGED
@@ -1,9 +1,10 @@
1
- import sprae, { directive, swap } from "../core.js";
1
+ import sprae, { directive } from "../core.js";
2
2
  import { _each } from './each.js';
3
+ import { effect } from "../signal.js";
3
4
 
4
5
  // :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
5
- // as for :if :scope - :if must init first, since it is lazy, to avoid initializing component ahead of time by :scope
6
- // we consider :scope={x} :if={x} case insignificant
6
+ // as for :if :with - :if must init first, since it is lazy, to avoid initializing component ahead of time by :with
7
+ // we consider :with={x} :if={x} case insignificant
7
8
  const _prevIf = Symbol("if");
8
9
  directive.if = (ifEl, evaluate, state) => {
9
10
  let parent = ifEl.parentNode,
@@ -25,14 +26,15 @@ directive.if = (ifEl, evaluate, state) => {
25
26
  else next.remove(), elses = next.content ? [...next.content.childNodes] : [next];
26
27
  } else elses = none
27
28
 
28
- return () => {
29
- const newEls = evaluate(state)?.valueOf() ? ifs : ifEl[_prevIf] ? none : elses;
29
+ return effect(() => {
30
+ const newEls = evaluate(state) ? ifs : ifEl[_prevIf] ? none : elses;
30
31
  if (next) next[_prevIf] = newEls === ifs
31
32
  if (cur != newEls) {
32
33
  // :if :each
33
34
  if (cur[0]?.[_each]) cur = [cur[0][_each]]
34
- swap(parent, cur, cur = newEls, holder);
35
- for (let el of cur) sprae(el, state);
35
+ for (let el of cur) el.remove();
36
+ cur = newEls;
37
+ for (let el of cur) parent.insertBefore(el, holder), sprae(el, state);
36
38
  }
37
- };
39
+ });
38
40
  };
package/directive/ref.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { directive } from "../core.js";
2
+ import { _change, _signals } from "../store.js";
2
3
  import { ipol } from './default.js';
3
4
 
4
5
  // ref must be last within primaries, since that must be skipped by :each, but before secondaries
5
- (directive.ref = (el, expr, state) => {
6
- let prev;
7
- return () => {
8
- if (prev) delete state[prev]
9
- state[prev = ipol(expr, state)] = el;
10
- }
11
- }).parse = expr => expr
6
+ directive.ref = (el, expr, state) => {
7
+ // defining prop is needed for inherited scope, like :each or :with, since el cannot be signal
8
+ Object.defineProperty(state, ipol(expr, state), { value: el })
9
+ }
10
+
11
+ directive.ref.parse = expr => expr
@@ -1,16 +1,17 @@
1
1
  import { directive } from "../core.js";
2
2
  import { ipol } from './default.js';
3
+ import { effect } from "../signal.js";
3
4
 
4
5
  directive.style = (el, evaluate, state) => {
5
6
  let initStyle = el.getAttribute("style") || "";
6
7
  if (!initStyle.endsWith(";")) initStyle += "; ";
7
8
 
8
- return () => {
9
- let v = evaluate(state)?.valueOf();
9
+ return effect(() => {
10
+ let v = evaluate(state);
10
11
  if (typeof v === "string") el.setAttribute("style", initStyle + ipol(v, state));
11
12
  else {
12
13
  el.setAttribute("style", initStyle);
13
14
  for (let k in v) el.style.setProperty(k, ipol(v[k], state));
14
15
  }
15
- };
16
+ });
16
17
  };
package/directive/text.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { directive } from "../core.js";
2
+ import { effect } from "../signal.js";
2
3
 
3
4
  // set text content
4
5
  directive.text = (el, evaluate, state) => {
5
6
  if (el.content) el.replaceWith(el = document.createTextNode('')) // <template :text="abc"/>
6
7
 
7
- return () => {
8
- let value = evaluate(state)?.valueOf();
8
+ return effect(() => {
9
+ let value = evaluate(state);
9
10
  el.textContent = value == null ? "" : value;
10
- };
11
+ });
11
12
  };
@@ -1,5 +1,6 @@
1
1
  import { directive } from "../core.js";
2
2
  import { attr } from './default.js';
3
+ import { effect } from "../signal.js";
3
4
 
4
5
  // connect expr to element value
5
6
  directive.value = (el, evaluate, state) => {
@@ -25,5 +26,5 @@ directive.value = (el, evaluate, state) => {
25
26
  }
26
27
  : (value) => (el.value = value);
27
28
 
28
- return () => (update(evaluate(state)?.valueOf?.()));
29
+ return effect(() => (update(evaluate(state))));
29
30
  };
@@ -0,0 +1,16 @@
1
+ import sprae, { directive } from "../core.js";
2
+ import store, { _signals } from '../store.js';
3
+ import { effect } from "../signal.js";
4
+
5
+ directive.with = (el, evaluate, rootState) => {
6
+ let state, values
7
+ return effect(() => {
8
+ values = evaluate(rootState);
9
+ Object.assign(state ||= sprae(el,
10
+ store(
11
+ values,
12
+ Object.create(rootState[_signals])
13
+ )
14
+ ), values)
15
+ })
16
+ };