sprae 11.0.7 → 11.1.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/core.js CHANGED
@@ -3,15 +3,31 @@ import store, { _signals } from './store.js';
3
3
 
4
4
  // polyfill
5
5
  const _dispose = (Symbol.dispose ||= Symbol("dispose"));
6
- export const _state = Symbol("state")
7
- export const _on = Symbol('on')
8
- export const _off = Symbol('off')
9
6
 
10
- // reserved directives - order matters!
11
- export const directive = {};
12
-
13
-
14
- // sprae element: apply directives
7
+ export const _state = Symbol("state"), _on = Symbol('on'), _off = Symbol('off')
8
+
9
+ // registered directives
10
+ const directive = {}
11
+
12
+ /**
13
+ * Register a directive with a parsed expression and evaluator.
14
+ * @param {string} name - The name of the directive.
15
+ * @param {(el: Element, state: Object, attrValue: string, attrName: string) => (value: any) => void} create - A function to create the directive.
16
+ * @param {(expr: string) => (state: Object) => any} [p=parse] - Create evaluator from expression string.
17
+ */
18
+ export const dir = (name, create, p = parse) => directive[name] = (el, expr, state, name, update, evaluate) => (
19
+ evaluate = p(expr),
20
+ update = create(el, state, expr, name, evaluate),
21
+ () => update(evaluate(state))
22
+ )
23
+
24
+ /**
25
+ * Applies directives to an HTML element and manages its reactive state.
26
+ *
27
+ * @param {Element} el - The target HTML element to apply directives to.
28
+ * @param {Object} [values] - Initial values to populate the element's reactive state.
29
+ * @returns {Object} The reactive state object associated with the element.
30
+ */
15
31
  export default function sprae(el, values) {
16
32
  // text nodes, comments etc
17
33
  if (!el?.childNodes) return
@@ -47,17 +63,16 @@ export default function sprae(el, values) {
47
63
 
48
64
  // init generic-name attributes second
49
65
  for (let i = 0; i < el.attributes?.length;) {
50
- let attr = el.attributes[i];
66
+ let attr = el.attributes[i], update;
51
67
 
52
68
  if (attr.name[0] === ':') {
53
69
  el.removeAttribute(attr.name);
54
70
 
55
71
  // multiple attributes like :id:for=""
56
72
  for (let name of attr.name.slice(1).split(':')) {
57
- let dir = directive[name] || directive.default,
58
- update = dir(el, (dir.parse || parse)(attr.value), state, name)
59
- fx.push(update)
60
- offs.push(effect(update))
73
+ update = (directive[name] || directive.default)(el, attr.value, state, name)
74
+
75
+ fx.push(update), offs.push(effect(update)) // save & start effect
61
76
 
62
77
  // stop after :each, :if, :with?
63
78
  if (_state in el) return
@@ -70,9 +85,17 @@ export default function sprae(el, values) {
70
85
  }
71
86
 
72
87
 
73
- // parse expression into evaluator fn
74
88
  const memo = {};
75
- export const parse = (expr, dir, fn) => {
89
+ /**
90
+ * Parses an expression into an evaluator function, caching the result for reuse.
91
+ *
92
+ * @param {string} expr - The expression to parse and compile into a function.
93
+ * @param {string} dir - The directive associated with the expression (used for error reporting).
94
+ * @returns {Function} The compiled evaluator function for the expression.
95
+ */
96
+ export const parse = (expr, dir) => {
97
+ let fn
98
+
76
99
  if (fn = memo[expr = expr.trim()]) return fn
77
100
 
78
101
  // static-time errors
@@ -83,11 +106,23 @@ export const parse = (expr, dir, fn) => {
83
106
  return memo[expr] = fn
84
107
  }
85
108
 
86
- // wrapped call
109
+ /**
110
+ * Branded sprae error with context about the directive and expression
111
+ *
112
+ * @param {Error} e - The original error object to enhance.
113
+ * @param {string} dir - The directive where the error occurred.
114
+ * @param {string} [expr=''] - The expression associated with the error, if any.
115
+ * @throws {Error} The enhanced error object with a formatted message.
116
+ */
87
117
  export const err = (e, dir, expr = '') => {
88
- throw Object.assign(e, { message: `∴ ${e.message}\n\n${dir}${expr ? `="${expr}"\n\n` : ""}`, expr })
118
+ throw Object.assign(e, { message: `∴ ${e.message}\n\n${dir || ''}${expr ? `="${expr}"\n\n` : ""}`, expr })
89
119
  }
90
120
 
121
+ /**
122
+ * Compiles an expression into an evaluator function.
123
+ *
124
+ * @type {(expr: string) => Function}
125
+ */
91
126
  export let compile
92
127
 
93
128
  // configure signals/compile
package/directive/aria.js CHANGED
@@ -1,9 +1,6 @@
1
- import { directive } from "../core.js";
1
+ import { dir } from "../core.js";
2
2
  import { attr, dashcase } from './default.js'
3
3
 
4
- directive['aria'] = (el, evaluate, state) => {
5
- const update = (value) => {
6
- for (let key in value) attr(el, 'aria-' + dashcase(key), value[key] == null ? null : value[key] + '');
7
- }
8
- return () => update(evaluate(state))
9
- }
4
+ dir('aria', (el) => value => {
5
+ for (let key in value) attr(el, 'aria-' + dashcase(key), value[key] == null ? null : value[key] + '')
6
+ })
@@ -1,9 +1,8 @@
1
- import { directive } from "../core.js";
1
+ import { dir } from "../core.js";
2
2
 
3
- directive.class = (el, evaluate, state) => {
4
- let cur = new Set
5
- return () => {
6
- let v = evaluate(state);
3
+ dir('class', (el, cur) => (
4
+ cur = new Set,
5
+ v => {
7
6
  let clsx = new Set;
8
7
  if (v) {
9
8
  if (typeof v === "string") v.split(' ').map(cls => clsx.add(cls));
@@ -12,5 +11,5 @@ directive.class = (el, evaluate, state) => {
12
11
  }
13
12
  for (let cls of cur) if (clsx.has(cls)) clsx.delete(cls); else el.classList.remove(cls);
14
13
  for (let cls of cur = clsx) el.classList.add(cls)
15
- };
16
- };
14
+ })
15
+ )
package/directive/data.js CHANGED
@@ -1,8 +1,3 @@
1
- import { directive } from "../core.js";
1
+ import { dir } from "../core.js";
2
2
 
3
- directive['data'] = (el, evaluate, state) => {
4
- return () => {
5
- let value = evaluate(state)
6
- for (let key in value) el.dataset[key] = value[key];
7
- }
8
- }
3
+ dir('data', el => value => {for (let key in value) el.dataset[key] = value[key];})
@@ -1,13 +1,12 @@
1
- import { directive, err } from "../core.js";
1
+ // generic property directive
2
+ import { dir, err } from "../core.js";
2
3
 
3
- // set generic property directive
4
- directive.default = (target, evaluate, state, name) => {
4
+ dir('default', (target, state, expr, name) => {
5
5
  // simple prop
6
- if (!name.startsWith('on')) return () => {
7
- let value = evaluate(state);
8
- if (name) attr(target, name, value)
9
- else for (let key in value) attr(target, dashcase(key), value[key]);
10
- };
6
+ if (!name.startsWith('on'))
7
+ return name ?
8
+ value => attr(target, name, value) :
9
+ value => { for (let key in value) attr(target, dashcase(key), value[key]) };
11
10
 
12
11
  // bind event to a target
13
12
  // NOTE: if you decide to remove chain of events, thing again - that's unique feature of sprae, don't diminish your own value.
@@ -15,30 +14,13 @@ directive.default = (target, evaluate, state, name) => {
15
14
  const ctxs = name.split('..').map(e => {
16
15
  let ctx = { evt: '', target, test: () => true };
17
16
  ctx.evt = (e.startsWith('on') ? e.slice(2) : e).replace(/\.(\w+)?-?([-\w]+)?/g,
18
- (match, mod, param = '') => (ctx.test = mods[mod]?.(ctx, ...param.split('-')) || ctx.test, '')
17
+ (_, mod, param = '') => (ctx.test = mods[mod]?.(ctx, ...param.split('-')) || ctx.test, '')
19
18
  );
20
19
  return ctx;
21
20
  });
22
21
 
23
- // single event
24
- if (ctxs.length == 1) return () => addListener(evaluate(state), ctxs[0])
25
-
26
- // events cycler
27
- let startFn, nextFn, off, idx = 0
28
- const nextListener = (fn) => {
29
- off = addListener((e) => (
30
- off(), nextFn = fn?.(e), (idx = ++idx % ctxs.length) ? nextListener(nextFn) : (startFn && nextListener(startFn))
31
- ), ctxs[idx]);
32
- }
33
-
34
- return () => (
35
- startFn = evaluate(state),
36
- !off && nextListener(startFn),
37
- () => startFn = null // nil startFn to autodispose chain
38
- )
39
-
40
22
  // add listener with the context
41
- function addListener(fn, { evt, target, test, defer, stop, prevent, immediate, ...opts }) {
23
+ const addListener = (fn, { evt, target, test, defer, stop, prevent, immediate, ...opts }) => {
42
24
  if (defer) fn = defer(fn)
43
25
 
44
26
  const cb = (e) => {
@@ -51,7 +33,23 @@ directive.default = (target, evaluate, state, name) => {
51
33
  return () => target.removeEventListener(evt, cb, opts)
52
34
  };
53
35
 
54
- };
36
+ // single event
37
+ if (ctxs.length == 1) return v => addListener(v, ctxs[0])
38
+
39
+ // events cycler
40
+ let startFn, nextFn, off, idx = 0
41
+ const nextListener = (fn) => {
42
+ off = addListener((e) => (
43
+ off(), nextFn = fn?.(e), (idx = ++idx % ctxs.length) ? nextListener(nextFn) : (startFn && nextListener(startFn))
44
+ ), ctxs[idx]);
45
+ }
46
+
47
+ return value => (
48
+ startFn = value,
49
+ !off && nextListener(startFn),
50
+ () => startFn = null // nil startFn to autodispose chain
51
+ )
52
+ })
55
53
 
56
54
  // event modifiers
57
55
  const mods = {
@@ -117,12 +115,6 @@ const keys = {
117
115
  char: (e) => /^\S$/.test(e.key),
118
116
  };
119
117
 
120
- // set attr
121
- export const attr = (el, name, v) => {
122
- if (v == null || v === false) el.removeAttribute(name);
123
- else el.setAttribute(name, v === true ? "" : typeof v === "number" || typeof v === "string" ? v : "");
124
- }
125
-
126
118
  // create delayed fns
127
119
  const throttle = (fn, limit) => {
128
120
  let pause, planned,
@@ -152,6 +144,12 @@ const debounce = (fn, wait) => {
152
144
  };
153
145
  };
154
146
 
147
+ // set attr
148
+ export const attr = (el, name, v) => {
149
+ if (v == null || v === false) el.removeAttribute(name);
150
+ else el.setAttribute(name, v === true ? "" : typeof v === "number" || typeof v === "string" ? v : "");
151
+ }
152
+
155
153
  export const dashcase = (str) => {
156
154
  return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match, i) => (i ? '-' : '') + match.toLowerCase());
157
155
  }
package/directive/each.js CHANGED
@@ -1,53 +1,37 @@
1
- import sprae, { _state, directive, frag, parse } from "../core.js";
1
+ import sprae, { _state, dir, frag, parse } from "../core.js";
2
2
  import store, { _change, _signals } from "../store.js";
3
- import { untracked, computed } from '../signal.js';
3
+ import { effect } from '../signal.js';
4
4
 
5
5
 
6
- directive.each = (tpl, [itemVar, idxVar, evaluate], state) => {
7
- // we need :if to be able to replace holder instead of tpl for :if :each case
8
- const holder = (document.createTextNode(""));
9
- tpl.replaceWith(holder);
10
- tpl[_state] = null // mark as fake-spraed, to preserve :-attribs for template
6
+ dir('each', (tpl, state, expr) => {
7
+ const [itemVar, idxVar = "$"] = expr.split(/\s+in\s+/)[0].split(/\s*,\s*/);
11
8
 
12
- // we re-create items any time new items are produced
13
- let cur, keys, prevl = 0
9
+ // we need :if to be able to replace holder instead of tpl for :if :each case
10
+ const holder = document.createTextNode("");
11
+ tpl.replaceWith(holder);
12
+ tpl[_state] = null // mark as fake-spraed, to preserve :-attribs for template
14
13
 
15
- // separate computed effect reduces number of needed updates for the effect
16
- const items = computed(() => {
17
- keys = null
18
- let items = evaluate(state)
19
- if (typeof items === "number") items = Array.from({ length: items }, (_, i) => i + 1)
20
- if (items?.constructor === Object) keys = Object.keys(items), items = Object.values(items)
21
- return items || []
22
- })
14
+ // we re-create items any time new items are produced
15
+ let cur, keys, items, prevl = 0
23
16
 
24
- const update = () => {
25
- // NOTE: untracked avoids rerendering full list whenever internal items or props change
26
- untracked(() => {
27
- let i = 0, newItems = items.value, newl = newItems.length
17
+ const update = () => {
18
+ let i = 0, newItems = items, newl = newItems.length
28
19
 
29
20
  // plain array update, not store (signal with array) - updates full list
30
- if (cur && !(cur[_change])) {
31
- for (let s of cur[_signals] || []) { s[Symbol.dispose]() }
21
+ if (cur && !cur[_change]) {
22
+ for (let s of cur[_signals] || []) s[Symbol.dispose]()
32
23
  cur = null, prevl = 0
33
24
  }
34
25
 
35
26
  // delete
36
- if (newl < prevl) {
37
- cur.length = newl
38
- }
27
+ if (newl < prevl) cur.length = newl
28
+
39
29
  // update, append, init
40
30
  else {
41
31
  // init
42
- if (!cur) {
43
- cur = newItems
44
- }
32
+ if (!cur) cur = newItems
45
33
  // update
46
- else {
47
- for (; i < prevl; i++) {
48
- cur[i] = newItems[i]
49
- }
50
- }
34
+ else while (i < prevl) cur[i] = newItems[i++]
51
35
 
52
36
  // append
53
37
  for (; i < newl; i++) {
@@ -70,24 +54,26 @@ directive.each = (tpl, [itemVar, idxVar, evaluate], state) => {
70
54
  }
71
55
 
72
56
  prevl = newl
73
- })
74
- }
75
-
76
- let planned = 0
77
- return () => {
78
- // subscribe to items change (.length) - we do it every time (not just on init) since preact unsubscribes unused signals
79
- items.value[_change]?.value
80
-
81
- // make first render immediately, debounce subsequent renders
82
- if (!planned++) update(), queueMicrotask(() => (planned > 1 && update(), planned = 0));
83
- }
84
- }
85
-
86
-
87
- // redefine parser to exclude `[a in] b`
88
- directive.each.parse = (expr) => {
89
- let [leftSide, itemsExpr] = expr.split(/\s+in\s+/);
90
- let [itemVar, idxVar = "$"] = leftSide.split(/\s*,\s*/);
91
-
92
- return [itemVar, idxVar, parse(itemsExpr)]
93
- }
57
+ }
58
+
59
+ return value => {
60
+ keys = null
61
+ if (typeof value === "number") items = Array.from({ length: value }, (_, i) => i + 1)
62
+ else if (value?.constructor === Object) keys = Object.keys(value), items = Object.values(value)
63
+ else items = value || []
64
+
65
+ // whenever list changes, we rebind internal change effect
66
+ let planned = 0
67
+ return effect(() => {
68
+ // subscribe to items change (.length) - we do it every time (not just in update) since preact unsubscribes unused signals
69
+ items[_change]?.value
70
+
71
+ // make first render immediately, debounce subsequent renders
72
+ if (!planned++) update(), queueMicrotask(() => (planned > 1 && update(), planned = 0));
73
+ })
74
+ }
75
+ },
76
+
77
+ // redefine evaluator to take second part of expression
78
+ expr => parse(expr.split(/\s+in\s+/)[1])
79
+ )
package/directive/fx.js CHANGED
@@ -1,5 +1,3 @@
1
- import { directive } from "../core.js";
1
+ import { dir } from "../core.js";
2
2
 
3
- directive.fx = (el, evaluate, state) => {
4
- return () => evaluate(state);
5
- };
3
+ dir('fx', _ => _ => _)
package/directive/if.js CHANGED
@@ -1,12 +1,14 @@
1
- import sprae, { directive, _state, _on, _off, frag } from "../core.js";
1
+ import sprae, { dir, _state, _on, _off, frag } from "../core.js";
2
2
 
3
3
  // :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
4
4
  // as for :if :with - :if must init first, since it is lazy, to avoid initializing component ahead of time by :with
5
5
  // we consider :with={x} :if={x} case insignificant
6
6
  const _prevIf = Symbol("if");
7
- directive.if = (el, evaluate, state) => {
7
+
8
+ dir('if', (el, state) => {
9
+ const holder = document.createTextNode('')
10
+
8
11
  let next = el.nextElementSibling,
9
- holder = document.createTextNode(''),
10
12
  curEl, ifEl, elseEl;
11
13
 
12
14
  el.replaceWith(holder)
@@ -20,8 +22,8 @@ directive.if = (el, evaluate, state) => {
20
22
  if (!next.hasAttribute(":if")) next.remove(), elseEl = next.content ? frag(next) : next, elseEl[_state] = null
21
23
  }
22
24
 
23
- return () => {
24
- const newEl = evaluate(state) ? ifEl : el[_prevIf] ? null : elseEl;
25
+ return (value) => {
26
+ const newEl = value ? ifEl : el[_prevIf] ? null : elseEl;
25
27
  if (next) next[_prevIf] = newEl === ifEl
26
28
  if (curEl != newEl) {
27
29
  // disable effects on child elements when element is not matched
@@ -34,4 +36,4 @@ directive.if = (el, evaluate, state) => {
34
36
  }
35
37
  }
36
38
  };
37
- };
39
+ })
package/directive/ref.js CHANGED
@@ -1,6 +1,9 @@
1
- import { directive } from "../core.js";
1
+ import { dir, parse } from "../core.js";
2
+ import { setter, ensure } from "./value.js";
2
3
 
3
- // ref must be last within primaries, since that must be skipped by :each, but before secondaries
4
- directive.ref = (el, evaluate, state) => {
5
- return () => evaluate(state)?.call?.(null, el)
6
- }
4
+ dir('ref', (el, state, expr, _, ev) => (
5
+ ensure(state, expr),
6
+ ev(state) == null ?
7
+ (setter(expr)(state, el), _ => _) :
8
+ v => v.call(null, el)
9
+ ))
@@ -1,14 +1,12 @@
1
- import { directive } from "../core.js";
1
+ import { dir } from "../core.js";
2
2
 
3
- directive.style = (el, evaluate, state) => {
4
- let initStyle = el.getAttribute("style");
5
-
6
- return () => {
7
- let v = evaluate(state);
3
+ dir('style', (el, initStyle) => (
4
+ initStyle = el.getAttribute("style"),
5
+ v => {
8
6
  if (typeof v === "string") el.setAttribute("style", initStyle + (initStyle.endsWith(';') ? '' : '; ') + v);
9
7
  else {
10
8
  if (initStyle) el.setAttribute("style", initStyle);
11
9
  for (let k in v) k[0] == '-' ? (el.style.setProperty(k, v[k])) : el.style[k] = v[k]
12
10
  }
13
- };
14
- };
11
+ })
12
+ )
package/directive/text.js CHANGED
@@ -1,12 +1,7 @@
1
- import { directive, frag } from "../core.js";
1
+ import { dir, frag } from "../core.js";
2
2
 
3
- // set text content
4
- directive.text = (el, evaluate, state) => {
3
+ dir('text', el => (
5
4
  // <template :text="a"/> or previously initialized template
6
- if (el.content) el.replaceWith(el = frag(el).childNodes[0])
7
-
8
- return () => {
9
- let value = evaluate(state);
10
- el.textContent = value == null ? "" : value;
11
- };
12
- };
5
+ el.content && el.replaceWith(el = frag(el).childNodes[0]),
6
+ value => el.textContent = value == null ? "" : value
7
+ ))
@@ -1,9 +1,9 @@
1
1
  import sprae from "../core.js";
2
- import { directive, parse } from "../core.js";
2
+ import { dir, parse } from "../core.js";
3
3
  import { attr } from './default.js';
4
4
 
5
- // connect expr to element value
6
- directive.value = (el, [getValue, setValue], state) => {
5
+
6
+ dir('value', (el, state, expr) => {
7
7
  const update =
8
8
  (el.type === "text" || el.type === "") ?
9
9
  (value) => el.setAttribute("value", (el.value = value == null ? "" : value)) :
@@ -29,36 +29,39 @@ directive.value = (el, [getValue, setValue], state) => {
29
29
  } :
30
30
  (value) => (el.value = value);
31
31
 
32
+ // make sure value exists in state
33
+ ensure(state, expr)
34
+
32
35
  // bind ui back to value
33
- const handleChange = el.type === 'checkbox' ? () => setValue(state, el.checked) : el.type === 'select-multiple' ? () => setValue(state, [...el.selectedOptions].map(o => o.value)) : (e) => setValue(state, el.selectedIndex < 0 ? null : el.value)
36
+ try {
37
+ const set = setter(expr)
38
+ const handleChange = el.type === 'checkbox' ? () => set(state, el.checked) :
39
+ el.type === 'select-multiple' ? () => set(state, [...el.selectedOptions].map(o => o.value)) :
40
+ () => set(state, el.selectedIndex < 0 ? null : el.value)
34
41
 
35
- el.oninput = el.onchange = handleChange; // hope user doesn't redefine these manually via `.oninput = somethingElse` - it saves 5 loc vs addEventListener
42
+ el.oninput = el.onchange = handleChange; // hope user doesn't redefine these manually via `.oninput = somethingElse` - it saves 5 loc vs addEventListener
36
43
 
37
- if (el.type?.startsWith('select')) {
38
- // select element also must observe any added/removed options or changed values (outside of sprae)
39
- new MutationObserver(handleChange).observe(el, { childList: true, subtree: true, attributes: true });
44
+ if (el.type?.startsWith('select')) {
45
+ // select element also must observe any added/removed options or changed values (outside of sprae)
46
+ new MutationObserver(handleChange).observe(el, { childList: true, subtree: true, attributes: true });
40
47
 
41
- // select options must be initialized before calling an update
42
- sprae(el, state)
43
- }
48
+ // select options must be initialized before calling an update
49
+ sprae(el, state)
50
+ }
51
+ } catch {}
44
52
 
45
- return () => {
46
- update(getValue(state));
47
- }
48
- };
53
+ return update
54
+ })
49
55
 
50
- directive.value.parse = expr => {
51
- let evaluate = [parse(expr)]
52
- // catch wrong assigns like `123 =...`, `foo?.bar =...`
53
- try {
54
- const set = parse(`${expr}=__`);
55
- // FIXME: if there's a simpler way to set value in justin?
56
- evaluate.push((state, value) => {
57
- state.__ = value
58
- set(state, value)
59
- delete state.__
60
- })
61
- }
62
- catch (e) { }
63
- return evaluate
64
- }
56
+ // create expression setter, reflecting value back to state
57
+ export const setter = (expr, set = parse(`${expr}=__`)) => (
58
+ // FIXME: if there's a simpler way to set value in justin?
59
+ (state, value) => (
60
+ state.__ = value,
61
+ set(state, value),
62
+ delete state.__
63
+ )
64
+ )
65
+
66
+ // make sure state contains first element of path, eg. `a` from `a.b[c]`
67
+ export const ensure = (state, expr, name = expr.match(/^\w+(?=\s*(?:\.|\[|$))/)) => name && (state[name[0]] ||= null)
package/directive/with.js CHANGED
@@ -1,10 +1,4 @@
1
- import sprae, { directive } from "../core.js";
1
+ import sprae, { dir } from "../core.js";
2
2
  import store, { _signals } from '../store.js';
3
3
 
4
- directive.with = (el, evaluate, rootState) => {
5
- let state
6
- return () => {
7
- let values = evaluate(rootState);
8
- sprae(el, state ? values : state = store(values, rootState))
9
- }
10
- };
4
+ dir('with', (el, rootState, state) => (state=null, values => sprae(el, state ? values : state = store(values, rootState))))