sprae 9.0.1 → 9.1.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,7 +1,3 @@
1
- import swapdom from 'swapdom'
2
- import * as signals from 'ulive'
3
- import justin from 'subscript/justin'
4
-
5
1
  // polyfill
6
2
  const _dispose = (Symbol.dispose ||= Symbol("dispose"));
7
3
 
@@ -9,7 +5,7 @@ const _dispose = (Symbol.dispose ||= Symbol("dispose"));
9
5
  const SPRAE = `∴`
10
6
 
11
7
  // signals impl
12
- export let { signal, effect, batch, computed, untracked } = signals;
8
+ export let signal, effect, batch, computed, untracked
13
9
 
14
10
  // reserved directives - order matters!
15
11
  export const directive = {};
@@ -49,7 +45,9 @@ export default function sprae(container, values) {
49
45
 
50
46
  // NOTE: secondary directives don't stop flow nor extend state, so no need to check
51
47
  for (let name of names) {
52
- let update = (directive[name] || directive.default)(el, attr.value, state, name);
48
+ let dir = directive[name] || directive.default
49
+ let evaluate = (dir.parse || parse)(attr.value, parse)
50
+ let update = dir(el, evaluate, state, name);
53
51
  if (update) {
54
52
  update[_dispose] = effect(update);
55
53
  effects.push(update);
@@ -83,6 +81,7 @@ export default function sprae(container, values) {
83
81
  while (effects.length) effects.pop()[_dispose]();
84
82
  container.classList.remove(SPRAE)
85
83
  memo.delete(container);
84
+ // NOTE: each child disposes own children etc.
86
85
  let els = container.getElementsByClassName(SPRAE);
87
86
  while (els.length) els[0][_dispose]?.()
88
87
  }
@@ -93,29 +92,26 @@ export default function sprae(container, values) {
93
92
  // default compiler
94
93
  const evalMemo = {};
95
94
 
96
- export let compile = (expr, dir, evaluate) => {
97
- if (evaluate = evalMemo[expr = expr.trim()]) return evaluate
95
+ const parse = (expr, dir, fn) => {
96
+ if (fn = evalMemo[expr = expr.trim()]) return fn
98
97
 
99
98
  // static-time errors
100
- try {
101
- // evaluate = new Function(`__scope`, `with (__scope) { return ${expr} };`);
102
- evaluate = justin(expr);
103
- }
104
- catch (e) { throw Object.assign(e, { message: `${SPRAE} ${e.message}\n\n${dir}${expr ? `="${expr}"\n\n` : ""}`, expr }) }
99
+ try { fn = compile(expr); }
100
+ catch (e) { throw Object.assign(e, { message: `∴ ${e.message}\n\n${dir}${expr ? `="${expr}"\n\n` : ""}`, expr }) }
101
+
102
+ fn.expr = expr
105
103
 
106
104
  // runtime errors
107
- return evalMemo[expr] = evaluate
105
+ return evalMemo[expr] = fn
108
106
  }
109
107
 
110
- // DOM swapper
111
- export let swap = swapdom
108
+ // compiler
109
+ export let compile
112
110
 
113
- // interpolate a$<b> fields from context
114
- export const ipol = (v, state) => {
115
- return v?.replace ? v.replace(/\$<([^>]+)>/g, (match, field) => state[field]?.valueOf?.() ?? '') : v
116
- };
111
+ // DOM swapper
112
+ export let swap
117
113
 
118
- // configure signals/compiler/differ
114
+ // configure signals/compile/differ
119
115
  // it's more compact than using sprae.signal = signal etc.
120
116
  sprae.use = s => {
121
117
  s.signal && (
@@ -126,4 +122,5 @@ sprae.use = s => {
126
122
  untracked = s.untracked || batch
127
123
  );
128
124
  s.swap && (swap = s.swap)
125
+ s.compile && (compile = s.compile)
129
126
  }
package/directive/aria.js CHANGED
@@ -1,8 +1,7 @@
1
- import { directive, compile } from "../core.js";
1
+ import { directive } from "../core.js";
2
2
  import { attr, dashcase } from './default.js'
3
3
 
4
- directive['aria'] = (el, expr, state) => {
5
- let evaluate = compile(expr, 'aria')
4
+ directive['aria'] = (el, evaluate, state) => {
6
5
  const update = (value) => {
7
6
  for (let key in value) attr(el, 'aria-' + dashcase(key), value[key] == null ? null : value[key] + '');
8
7
  }
@@ -1,7 +1,7 @@
1
- import { directive, compile, ipol } from "../core.js";
1
+ import { directive } from "../core.js";
2
+ import { ipol } from './default.js';
2
3
 
3
- directive.class = (el, expr, state) => {
4
- let evaluate = compile(expr, 'class');
4
+ directive.class = (el, evaluate, state) => {
5
5
  let cur = new Set
6
6
  return () => {
7
7
  let v = evaluate(state);
package/directive/data.js CHANGED
@@ -1,8 +1,6 @@
1
- import { directive, compile } from "../core.js";
2
-
3
- directive['data'] = (el, expr, state) => {
4
- let evaluate = compile(expr, 'data')
1
+ import { directive } from "../core.js";
5
2
 
3
+ directive['data'] = (el, evaluate, state) => {
6
4
  return () => {
7
5
  let value = evaluate(state)?.valueOf()
8
6
  for (let key in value) el.dataset[key] = value[key];
@@ -1,15 +1,14 @@
1
- import { directive, compile, ipol } from "../core.js";
1
+ import { directive } from "../core.js";
2
2
 
3
3
  // set generic property directive
4
- directive.default = (el, expr, state, name) => {
4
+ directive.default = (el, evaluate, state, name) => {
5
5
  let evt = name.startsWith("on") && name.slice(2);
6
- let evaluate = compile(expr, name);
7
6
 
8
7
  if (evt) {
9
8
  let off
10
9
  return () => (
11
10
  off?.(), // intermediate teardown
12
- off = on(el, evt, evaluate(state))
11
+ off = on(el, evt, evaluate(state)?.valueOf())
13
12
  );
14
13
  }
15
14
 
@@ -146,3 +145,8 @@ const debounce = (fn, wait) => {
146
145
  export const dashcase = (str) => {
147
146
  return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => "-" + match.toLowerCase());
148
147
  }
148
+
149
+ // interpolate a$<b> fields from context
150
+ export const ipol = (v, state) => {
151
+ return v?.replace ? v.replace(/\$<([^>]+)>/g, (match, field) => state[field]?.valueOf?.() ?? '') : v
152
+ };
package/directive/each.js CHANGED
@@ -1,61 +1,61 @@
1
- import sprae, { directive, compile, swap } from "../core.js";
1
+ import sprae, { directive, swap } from "../core.js";
2
2
 
3
3
  export const _each = Symbol(":each");
4
4
 
5
- const keys = {}; // boxed primitives pool
5
+ const keys = {}, _key = Symbol('key');
6
6
 
7
7
  // :each must init before :ref, :id or any others, since it defines scope
8
- directive.each = (tpl, expr, state, name) => {
9
- let [leftSide, itemsExpr] = expr.split(/\s+in\s+/);
10
- let [itemVar, idxVar = "_$"] = leftSide.split(/\s*,\s*/);
11
-
8
+ (directive.each = (tpl, [itemVar, idxVar, evaluate], state) => {
12
9
  // we need :if to be able to replace holder instead of tpl for :if :each case
13
- const holder = (tpl[_each] = document.createTextNode(""));
10
+ const holder = (tpl[_each] = document.createTextNode("")), parent = tpl.parentNode;
14
11
  tpl.replaceWith(holder);
15
12
 
16
- const evaluate = compile(itemsExpr, name);
17
- const memo = new WeakMap;
18
-
19
- tpl.removeAttribute(':key')
13
+ // key -> el
14
+ const elCache = new WeakMap, stateCache = new WeakMap
20
15
 
21
16
  let cur = [];
22
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
23
30
  return () => {
24
- // naive approach: whenever items change we replace full list
25
31
  let items = evaluate(state)?.valueOf(), els = [];
26
- if (typeof items === "number") items = Array.from({ length: items }, (_, i) => i);
27
32
 
28
- const count = new WeakSet
29
- for (let idx in items) {
30
- let item = items[idx]
31
- // creating via prototype is faster in both creation time & reading time
32
- let substate = Object.create(state, { [idxVar]: { value: idx } });
33
- substate[itemVar] = item; // can be changed by subsequent updates, need to be writable
34
- let el, key = (item.key ?? item.id ?? item); // NOTE: no need to unwrap singnal, since item fallback covers it
33
+ if (typeof items === "number") items = Array.from({ length: items }, (_, i) => i)
35
34
 
36
- if (key == null) el = tpl.cloneNode(true)
37
- else {
38
- // make sure key is object
39
- if (Object(key) !== key) key = (keys[key] ||= Object(key));
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
40
 
41
- if (count.has(key)) {
42
- console.warn('Duplicate key', key), el = tpl.cloneNode(true);
43
- }
44
- else {
45
- count.add(key);
46
- el = memo.get(key) || memo.set(key, tpl.cloneNode(true)).get(key);
47
- }
48
- }
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;
49
43
 
50
- if (el.content) el = el.content.cloneNode(true) // <template>
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
51
47
 
52
- sprae(el, substate)
48
+ sprae(el, substate);
53
49
 
54
50
  // document fragment
55
- if (el.nodeType === 11) els.push(...el.childNodes);
56
- else els.push(el);
51
+ if (el.nodeType === 11) els.push(...el.childNodes); else els.push(el);
57
52
  }
58
53
 
59
- swap(holder.parentNode, cur, cur = els, holder);
60
- };
61
- };
54
+ swap(parent, cur, cur = els, holder, options);
55
+ }
56
+ }).parse = (expr, parse) => {
57
+ let [leftSide, itemsExpr] = expr.split(/\s+in\s+/);
58
+ let [itemVar, idxVar = "$"] = leftSide.split(/\s*,\s*/);
59
+
60
+ return [itemVar, idxVar, parse(itemsExpr)]
61
+ }
package/directive/fx.js CHANGED
@@ -1,6 +1,5 @@
1
- import { directive, compile } from "../core.js";
1
+ import { directive } from "../core.js";
2
2
 
3
- directive.fx = (el, expr, state, name) => {
4
- let evaluate = compile(expr, name);
3
+ directive.fx = (el, evaluate, state) => {
5
4
  return () => evaluate(state);
6
5
  };
package/directive/html.js CHANGED
@@ -1,7 +1,7 @@
1
- import sprae, { directive, compile } from "../core.js";
1
+ import sprae, { directive } from "../core.js";
2
2
 
3
- directive.html = (el, expr, state, name) => {
4
- let evaluate = compile(expr, name), tpl = evaluate(state);
3
+ directive.html = (el, evaluate, state) => {
4
+ let tpl = evaluate(state);
5
5
 
6
6
  if (!tpl) return
7
7
 
package/directive/if.js CHANGED
@@ -1,17 +1,15 @@
1
- import sprae, { compile, directive, swap } from "../core.js";
1
+ import sprae, { directive, swap } from "../core.js";
2
2
  import { _each } from './each.js';
3
3
 
4
4
  // :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
5
5
  // as for :if :scope - :if must init first, since it is lazy, to avoid initializing component ahead of time by :scope
6
6
  // we consider :scope={x} :if={x} case insignificant
7
7
  const _prevIf = Symbol("if");
8
- directive.if = (ifEl, expr, state, name) => {
8
+ directive.if = (ifEl, evaluate, state) => {
9
9
  let parent = ifEl.parentNode,
10
10
  next = ifEl.nextElementSibling,
11
11
  holder = document.createTextNode(''),
12
12
 
13
- evaluate = compile(expr, name),
14
-
15
13
  // actual replaceable els (takes <template>)
16
14
  cur, ifs, elses, none = [];
17
15
 
package/directive/ref.js CHANGED
@@ -1,10 +1,11 @@
1
- import { directive, ipol } from "../core.js";
1
+ import { directive } from "../core.js";
2
+ import { ipol } from './default.js';
2
3
 
3
4
  // ref must be last within primaries, since that must be skipped by :each, but before secondaries
4
- directive.ref = (el, expr, state) => {
5
+ (directive.ref = (el, expr, state) => {
5
6
  let prev;
6
7
  return () => {
7
8
  if (prev) delete state[prev]
8
9
  state[prev = ipol(expr, state)] = el;
9
10
  }
10
- };
11
+ }).parse = expr => expr
@@ -1,9 +1,8 @@
1
- import sprae, { directive, compile } from "../core.js";
1
+ import sprae, { directive } from "../core.js";
2
2
 
3
3
  // `:each` can redefine scope as `:each="a in {myScope}"`,
4
4
  // same time per-item scope as `:each="..." :scope="{collapsed:true}"` is useful
5
- directive.scope = (el, expr, rootState, name) => {
6
- let evaluate = compile(expr, name);
5
+ directive.scope = (el, evaluate, rootState) => {
7
6
  // local state may contain signals that update, so we take them over
8
7
  return () => {
9
8
  sprae(el, { ...rootState, ...(evaluate(rootState)?.valueOf?.() || {}) });
@@ -1,7 +1,7 @@
1
- import { directive, compile, ipol } from "../core.js";
1
+ import { directive } from "../core.js";
2
+ import { ipol } from './default.js';
2
3
 
3
- directive.style = (el, expr, state) => {
4
- let evaluate = compile(expr, 'style');
4
+ directive.style = (el, evaluate, state) => {
5
5
  let initStyle = el.getAttribute("style") || "";
6
6
  if (!initStyle.endsWith(";")) initStyle += "; ";
7
7
 
package/directive/text.js CHANGED
@@ -1,8 +1,7 @@
1
- import { directive, compile } from "../core.js";
1
+ import { directive } from "../core.js";
2
2
 
3
3
  // set text content
4
- directive.text = (el, expr, state) => {
5
- let evaluate = compile(expr, 'text');
4
+ directive.text = (el, evaluate, state) => {
6
5
  if (el.content) el.replaceWith(el = document.createTextNode('')) // <template :text="abc"/>
7
6
 
8
7
  return () => {
@@ -1,10 +1,8 @@
1
- import { directive, compile } from "../core.js";
1
+ import { directive } from "../core.js";
2
2
  import { attr } from './default.js';
3
3
 
4
4
  // connect expr to element value
5
- directive.value = (el, expr, state) => {
6
- let evaluate = compile(expr, 'value');
7
-
5
+ directive.value = (el, evaluate, state) => {
8
6
  let from, to;
9
7
  let update = el.type === "text" || el.type === ""
10
8
  ? (value) => el.setAttribute("value", (el.value = value == null ? "" : value))