sprae 11.0.7 → 11.0.8

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,28 @@ 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), update = create(el, state, expr, name), () => update(evaluate(state)))
20
+
21
+ /**
22
+ * Applies directives to an HTML element and manages its reactive state.
23
+ *
24
+ * @param {Element} el - The target HTML element to apply directives to.
25
+ * @param {Object} [values] - Initial values to populate the element's reactive state.
26
+ * @returns {Object} The reactive state object associated with the element.
27
+ */
15
28
  export default function sprae(el, values) {
16
29
  // text nodes, comments etc
17
30
  if (!el?.childNodes) return
@@ -47,17 +60,16 @@ export default function sprae(el, values) {
47
60
 
48
61
  // init generic-name attributes second
49
62
  for (let i = 0; i < el.attributes?.length;) {
50
- let attr = el.attributes[i];
63
+ let attr = el.attributes[i], update;
51
64
 
52
65
  if (attr.name[0] === ':') {
53
66
  el.removeAttribute(attr.name);
54
67
 
55
68
  // multiple attributes like :id:for=""
56
69
  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))
70
+ update = (directive[name] || directive.default)(el, attr.value, state, name)
71
+
72
+ fx.push(update), offs.push(effect(update)) // save & start effect
61
73
 
62
74
  // stop after :each, :if, :with?
63
75
  if (_state in el) return
@@ -70,9 +82,17 @@ export default function sprae(el, values) {
70
82
  }
71
83
 
72
84
 
73
- // parse expression into evaluator fn
74
85
  const memo = {};
75
- export const parse = (expr, dir, fn) => {
86
+ /**
87
+ * Parses an expression into an evaluator function, caching the result for reuse.
88
+ *
89
+ * @param {string} expr - The expression to parse and compile into a function.
90
+ * @param {string} dir - The directive associated with the expression (used for error reporting).
91
+ * @returns {Function} The compiled evaluator function for the expression.
92
+ */
93
+ export const parse = (expr, dir) => {
94
+ let fn
95
+
76
96
  if (fn = memo[expr = expr.trim()]) return fn
77
97
 
78
98
  // static-time errors
@@ -83,11 +103,23 @@ export const parse = (expr, dir, fn) => {
83
103
  return memo[expr] = fn
84
104
  }
85
105
 
86
- // wrapped call
106
+ /**
107
+ * Branded sprae error with context about the directive and expression
108
+ *
109
+ * @param {Error} e - The original error object to enhance.
110
+ * @param {string} dir - The directive where the error occurred.
111
+ * @param {string} [expr=''] - The expression associated with the error, if any.
112
+ * @throws {Error} The enhanced error object with a formatted message.
113
+ */
87
114
  export const err = (e, dir, expr = '') => {
88
115
  throw Object.assign(e, { message: `∴ ${e.message}\n\n${dir}${expr ? `="${expr}"\n\n` : ""}`, expr })
89
116
  }
90
117
 
118
+ /**
119
+ * Compiles an expression into an evaluator function.
120
+ *
121
+ * @type {(expr: string) => Function}
122
+ */
91
123
  export let compile
92
124
 
93
125
  // 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,5 @@
1
- import { directive } from "../core.js";
1
+ import { dir } from "../core.js";
2
2
 
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
- }
3
+ dir('ref', (el, state, expr) => (
4
+ v => v.call(null, el)
5
+ ))
@@ -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)) :
@@ -30,7 +30,10 @@ directive.value = (el, [getValue, setValue], state) => {
30
30
  (value) => (el.value = value);
31
31
 
32
32
  // 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)
33
+ let set = setter(expr)
34
+ const handleChange = el.type === 'checkbox' ? () => set(state, el.checked) :
35
+ el.type === 'select-multiple' ? () => set(state, [...el.selectedOptions].map(o => o.value)) :
36
+ () => set(state, el.selectedIndex < 0 ? null : el.value)
34
37
 
35
38
  el.oninput = el.onchange = handleChange; // hope user doesn't redefine these manually via `.oninput = somethingElse` - it saves 5 loc vs addEventListener
36
39
 
@@ -42,23 +45,20 @@ directive.value = (el, [getValue, setValue], state) => {
42
45
  sprae(el, state)
43
46
  }
44
47
 
45
- return () => {
46
- update(getValue(state));
47
- }
48
- };
48
+ return update
49
+ })
49
50
 
50
- directive.value.parse = expr => {
51
- let evaluate = [parse(expr)]
51
+ // create expression setter, reflecting value back to state
52
+ export const setter = expr => {
52
53
  // catch wrong assigns like `123 =...`, `foo?.bar =...`
53
54
  try {
54
55
  const set = parse(`${expr}=__`);
55
56
  // FIXME: if there's a simpler way to set value in justin?
56
- evaluate.push((state, value) => {
57
+ return (state, value) => {
57
58
  state.__ = value
58
59
  set(state, value)
59
60
  delete state.__
60
- })
61
+ }
61
62
  }
62
63
  catch (e) { }
63
- return evaluate
64
64
  }
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))))