sprae 11.0.6 → 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
@@ -4,94 +4,122 @@ import store, { _signals } from './store.js';
4
4
  // polyfill
5
5
  const _dispose = (Symbol.dispose ||= Symbol("dispose"));
6
6
 
7
-
8
- // reserved directives - order matters!
9
- export const directive = {};
10
-
11
- // every element that's in cache === directly spraed and un subsequent sprae is just updated (like each)
12
- export const memo = new WeakMap();
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
18
31
 
19
32
  // repeated call can be caused by :each with new objects with old keys needs an update
20
- if (memo.has(el)) {
33
+ if (_state in el) {
21
34
  // we rewrite signals instead of update, because user should have what he provided
22
- return Object.assign(memo.get(el), values)
35
+ return Object.assign(el[_state], values)
23
36
  }
24
37
 
25
38
  // take over existing state instead of creating clone
26
- const state = store(values || {}), disposes = []
39
+ const state = store(values || {}), offs = [], fx = []
27
40
 
28
41
  init(el);
29
42
 
30
- // if element was spraed by :with or :each instruction - skip, otherwise save
31
- if (!memo.has(el)) memo.set(el, state);
43
+ // if element was spraed by inline :with instruction (meaning it has extended state) - skip, otherwise save _state
44
+ if (!(_state in el)) {
45
+ el[_state] = state
46
+
47
+ // on/off all effects
48
+ el[_off] = () => { while (offs.length) offs.pop()() }
49
+ el[_on] = () => offs.push(...fx.map(f => effect(f)))
32
50
 
33
- // disposer unspraes all internal elements
34
- el[_dispose] = () => {
35
- while (disposes.length) disposes.pop()();
36
- memo.delete(el);
37
- el[_dispose] = null;
51
+ // destroy
52
+ el[_dispose] = () => (el[_off](), el[_off] = el[_on] = el[_dispose] = el[_state] = null)
38
53
  }
39
54
 
40
55
  return state;
41
56
 
42
- function init(el, parent = el.parentNode) {
43
- if (!el.childNodes) return // ignore text nodes, comments etc
57
+ function init(el) {
58
+ // ignore text nodes, comments etc
59
+ if (!el.childNodes) return
60
+
44
61
  // init generic-name attributes second
45
62
  for (let i = 0; i < el.attributes?.length;) {
46
- let attr = el.attributes[i];
63
+ let attr = el.attributes[i], update;
47
64
 
48
65
  if (attr.name[0] === ':') {
49
66
  el.removeAttribute(attr.name);
50
67
 
51
68
  // multiple attributes like :id:for=""
52
- let names = attr.name.slice(1).split(':')
53
-
54
- for (let name of names) {
55
- let dir = directive[name] || directive.default
56
- let evaluate = (dir.parse || parse)(attr.value)
57
- let fn = dir(el, evaluate, state, name);
58
- if (fn) disposes.push(effect(fn))
59
- disposes.push(() => el.setAttributeNode(attr)) // recover attribute
60
- }
69
+ for (let name of attr.name.slice(1).split(':')) {
70
+ update = (directive[name] || directive.default)(el, attr.value, state, name)
61
71
 
62
- // stop if element was spraed by internal directive
63
- if (memo.has(el)) return el[_dispose] && disposes.push(el[_dispose])
72
+ fx.push(update), offs.push(effect(update)) // save & start effect
64
73
 
65
- // stop if element is skipped/detached like in case of :if or :each
66
- if (el.parentNode !== parent) return
74
+ // stop after :each, :if, :with?
75
+ if (_state in el) return
76
+ }
67
77
  } else i++;
68
78
  }
69
79
 
70
- for (let child of [...el.childNodes])
71
- // adjust for template container - parent is overlooked
72
- init(child, el.content ? el.childNodes[0].parentNode : el);
80
+ for (let child of [...el.childNodes]) init(child);
73
81
  };
74
82
  }
75
83
 
76
84
 
77
- // parse expression into evaluator fn
78
- const evalMemo = {};
79
- export const parse = (expr, dir, fn) => {
80
- if (fn = evalMemo[expr = expr.trim()]) return fn
85
+ const memo = {};
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
+
96
+ if (fn = memo[expr = expr.trim()]) return fn
81
97
 
82
98
  // static-time errors
83
99
  try { fn = compile(expr) }
84
100
  catch (e) { err(e, dir, expr) }
85
101
 
86
102
  // runtime errors
87
- return evalMemo[expr] = fn
103
+ return memo[expr] = fn
88
104
  }
89
105
 
90
- // 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
+ */
91
114
  export const err = (e, dir, expr = '') => {
92
115
  throw Object.assign(e, { message: `∴ ${e.message}\n\n${dir}${expr ? `="${expr}"\n\n` : ""}`, expr })
93
116
  }
94
117
 
118
+ /**
119
+ * Compiles an expression into an evaluator function.
120
+ *
121
+ * @type {(expr: string) => Function}
122
+ */
95
123
  export let compile
96
124
 
97
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,13 +14,27 @@ 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
 
22
+ // add listener with the context
23
+ const addListener = (fn, { evt, target, test, defer, stop, prevent, immediate, ...opts }) => {
24
+ if (defer) fn = defer(fn)
25
+
26
+ const cb = (e) => {
27
+ try {
28
+ test(e) && (stop && (immediate ? e.stopImmediatePropagation() : e.stopPropagation()), prevent && e.preventDefault(), fn?.call(state, e))
29
+ } catch (error) { err(error, `:on${evt}`, fn) }
30
+ };
31
+
32
+ target.addEventListener(evt, cb, opts)
33
+ return () => target.removeEventListener(evt, cb, opts)
34
+ };
35
+
23
36
  // single event
24
- if (ctxs.length == 1) return () => addListener(evaluate(state), ctxs[0])
37
+ if (ctxs.length == 1) return v => addListener(v, ctxs[0])
25
38
 
26
39
  // events cycler
27
40
  let startFn, nextFn, off, idx = 0
@@ -31,27 +44,12 @@ directive.default = (target, evaluate, state, name) => {
31
44
  ), ctxs[idx]);
32
45
  }
33
46
 
34
- return () => (
35
- startFn = evaluate(state),
47
+ return value => (
48
+ startFn = value,
36
49
  !off && nextListener(startFn),
37
50
  () => startFn = null // nil startFn to autodispose chain
38
51
  )
39
-
40
- // add listener with the context
41
- function addListener(fn, { evt, target, test, defer, stop, prevent, immediate, ...opts }) {
42
- if (defer) fn = defer(fn)
43
-
44
- const cb = (e) => {
45
- try {
46
- test(e) && (stop && (immediate ? e.stopImmediatePropagation() : e.stopPropagation()), prevent && e.preventDefault(), fn?.(e))
47
- } catch (error) { err(error, `:on${evt}`, fn) }
48
- };
49
-
50
- target.addEventListener(evt, cb, opts)
51
- return () => target.removeEventListener(evt, cb, opts)
52
- };
53
-
54
- };
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,52 +1,37 @@
1
- import sprae, { 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);
6
+ dir('each', (tpl, state, expr) => {
7
+ const [itemVar, idxVar = "$"] = expr.split(/\s+in\s+/)[0].split(/\s*,\s*/);
10
8
 
11
- // we re-create items any time new items are produced
12
- 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
13
13
 
14
- // separate computed effect reduces number of needed updates for the effect
15
- const items = computed(() => {
16
- keys = null
17
- let items = evaluate(state)
18
- if (typeof items === "number") items = Array.from({ length: items }, (_, i) => i + 1)
19
- if (items?.constructor === Object) keys = Object.keys(items), items = Object.values(items)
20
- return items || []
21
- })
14
+ // we re-create items any time new items are produced
15
+ let cur, keys, items, prevl = 0
22
16
 
23
- const update = () => {
24
- // NOTE: untracked avoids rerendering full list whenever internal items or props change
25
- untracked(() => {
26
- let i = 0, newItems = items.value, newl = newItems.length
17
+ const update = () => {
18
+ let i = 0, newItems = items, newl = newItems.length
27
19
 
28
20
  // plain array update, not store (signal with array) - updates full list
29
- if (cur && !(cur[_change])) {
30
- for (let s of cur[_signals] || []) { s[Symbol.dispose]() }
21
+ if (cur && !cur[_change]) {
22
+ for (let s of cur[_signals] || []) s[Symbol.dispose]()
31
23
  cur = null, prevl = 0
32
24
  }
33
25
 
34
26
  // delete
35
- if (newl < prevl) {
36
- cur.length = newl
37
- }
27
+ if (newl < prevl) cur.length = newl
28
+
38
29
  // update, append, init
39
30
  else {
40
31
  // init
41
- if (!cur) {
42
- cur = newItems
43
- }
32
+ if (!cur) cur = newItems
44
33
  // update
45
- else {
46
- for (; i < prevl; i++) {
47
- cur[i] = newItems[i]
48
- }
49
- }
34
+ else while (i < prevl) cur[i] = newItems[i++]
50
35
 
51
36
  // append
52
37
  for (; i < newl; i++) {
@@ -69,24 +54,26 @@ directive.each = (tpl, [itemVar, idxVar, evaluate], state) => {
69
54
  }
70
55
 
71
56
  prevl = newl
72
- })
73
- }
74
-
75
- let planned = 0
76
- return () => {
77
- // subscribe to items change (.length) - we do it every time (not just on init) since preact unsubscribes unused signals
78
- items.value[_change]?.value
79
-
80
- // make first render immediately, debounce subsequent renders
81
- if (!planned++) update(), queueMicrotask(() => (planned > 1 && update(), planned = 0));
82
- }
83
- }
84
-
85
-
86
- // redefine parser to exclude `[a in] b`
87
- directive.each.parse = (expr) => {
88
- let [leftSide, itemsExpr] = expr.split(/\s+in\s+/);
89
- let [itemVar, idxVar = "$"] = leftSide.split(/\s*,\s*/);
90
-
91
- return [itemVar, idxVar, parse(itemsExpr)]
92
- }
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,36 +1,39 @@
1
- import sprae, { directive, memo, 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)
13
15
 
14
16
  ifEl = el.content ? frag(el) : el
15
- memo.set(ifEl, null) // mark all el as fake-spraed, because we have to sprae for real on insert
17
+ ifEl[_state] = null // mark el as fake-spraed to holdon init, since we sprae rest when branch matches
16
18
 
17
19
  if (next?.hasAttribute(":else")) {
18
20
  next.removeAttribute(":else");
19
21
  // if next is :else :if - leave it for its own :if handler
20
- if (!next.hasAttribute(":if")) next.remove(), elseEl = next.content ? frag(next) : next, memo.set(elseEl, null)
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
- // disable effects on child elements to avoid internal effects from triggering on value changes when element's not matched
28
- if (curEl) curEl.remove(), curEl[Symbol.dispose]?.();
29
+ // disable effects on child elements when element is not matched
30
+ if (curEl) curEl.remove(), curEl[_off]?.();
29
31
  if (curEl = newEl) {
30
32
  holder.before(curEl.content || curEl)
31
- memo.get(curEl) === null && memo.delete(curEl) // remove fake memo to sprae as new
32
- sprae(curEl, state)
33
+ curEl[_state] === null && delete curEl[_state] // remove fake memo to sprae as new
34
+ // enable effects if branch is matched
35
+ curEl[_state] ? curEl[_on]() : sprae(curEl, state)
33
36
  }
34
37
  }
35
38
  };
36
- };
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))))