sprae 12.3.9 → 12.4.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.
Files changed (79) hide show
  1. package/core.js +236 -59
  2. package/directive/_.js +8 -0
  3. package/directive/class.js +9 -0
  4. package/directive/each.js +41 -17
  5. package/directive/else.js +6 -2
  6. package/directive/event.js +9 -0
  7. package/directive/fx.js +5 -0
  8. package/directive/hidden.js +6 -0
  9. package/directive/html.js +29 -6
  10. package/directive/if.js +7 -2
  11. package/directive/portal.js +38 -0
  12. package/directive/ref.js +8 -0
  13. package/directive/scope.js +7 -0
  14. package/directive/sequence.js +9 -1
  15. package/directive/spread.js +6 -0
  16. package/directive/style.js +9 -0
  17. package/directive/text.js +5 -0
  18. package/directive/value.js +13 -3
  19. package/dist/sprae.js +3 -6
  20. package/dist/sprae.js.map +4 -4
  21. package/dist/sprae.umd.js +3 -6
  22. package/dist/sprae.umd.js.map +4 -4
  23. package/package.json +35 -8
  24. package/readme.md +39 -54
  25. package/signal.js +41 -3
  26. package/sprae.js +117 -21
  27. package/store.js +54 -13
  28. package/types/core.d.ts +222 -0
  29. package/types/core.d.ts.map +1 -0
  30. package/types/directive/_.d.ts +3 -0
  31. package/types/directive/_.d.ts.map +1 -0
  32. package/types/directive/aria.d.ts.map +1 -0
  33. package/types/directive/class.d.ts +3 -0
  34. package/types/directive/class.d.ts.map +1 -0
  35. package/types/directive/data.d.ts.map +1 -0
  36. package/types/directive/each.d.ts +6 -0
  37. package/types/directive/each.d.ts.map +1 -0
  38. package/types/directive/else.d.ts +3 -0
  39. package/types/directive/else.d.ts.map +1 -0
  40. package/types/directive/event.d.ts +5 -0
  41. package/types/directive/event.d.ts.map +1 -0
  42. package/types/directive/fx.d.ts +3 -0
  43. package/types/directive/fx.d.ts.map +1 -0
  44. package/types/directive/hidden.d.ts +3 -0
  45. package/types/directive/hidden.d.ts.map +1 -0
  46. package/types/directive/html.d.ts +3 -0
  47. package/types/directive/html.d.ts.map +1 -0
  48. package/types/directive/if.d.ts +3 -0
  49. package/types/directive/if.d.ts.map +1 -0
  50. package/types/directive/item.d.ts.map +1 -0
  51. package/types/directive/portal.d.ts +3 -0
  52. package/types/directive/portal.d.ts.map +1 -0
  53. package/types/directive/ref.d.ts +5 -0
  54. package/types/directive/ref.d.ts.map +1 -0
  55. package/types/directive/resize.d.ts.map +1 -0
  56. package/types/directive/scope.d.ts +3 -0
  57. package/types/directive/scope.d.ts.map +1 -0
  58. package/types/directive/sequence.d.ts +5 -0
  59. package/types/directive/sequence.d.ts.map +1 -0
  60. package/types/directive/spread.d.ts +3 -0
  61. package/types/directive/spread.d.ts.map +1 -0
  62. package/types/directive/style.d.ts +3 -0
  63. package/types/directive/style.d.ts.map +1 -0
  64. package/types/directive/text.d.ts +3 -0
  65. package/types/directive/text.d.ts.map +1 -0
  66. package/types/directive/value.d.ts +4 -0
  67. package/types/directive/value.d.ts.map +1 -0
  68. package/types/signal.d.ts +6 -0
  69. package/types/signal.d.ts.map +1 -0
  70. package/types/sprae.d.ts +15 -0
  71. package/types/sprae.d.ts.map +1 -0
  72. package/types/store.d.ts +15 -0
  73. package/types/store.d.ts.map +1 -0
  74. package/directive/aria.js +0 -0
  75. package/directive/data.js +0 -0
  76. package/directive/item.js +0 -0
  77. package/dist/sprae.micro.js +0 -6
  78. package/dist/sprae.micro.js.map +0 -7
  79. package/micro.js +0 -56
package/core.js CHANGED
@@ -1,23 +1,142 @@
1
1
  import store, { _change, _signals } from "./store.js";
2
2
 
3
- export const _dispose = (Symbol.dispose ||= Symbol("dispose")),
4
- _state = Symbol("state"),
5
- _on = Symbol('on'),
6
- _off = Symbol('off'),
7
- _add = Symbol('init');
3
+ /** Symbol for disposal (using standard Symbol.dispose if available) */
4
+ export const _dispose = (Symbol.dispose ||= Symbol("dispose"))
8
5
 
9
- export let prefix = ':', signal, effect, computed, batch = (fn) => fn(), untracked = batch;
6
+ /** Symbol for accessing element's reactive state */
7
+ export const _state = Symbol("state")
10
8
 
11
- export let directive = {}, modifier = {}
9
+ /** Symbol for enabling element effects */
10
+ export const _on = Symbol('on')
11
+
12
+ /** Symbol for disabling element effects */
13
+ export const _off = Symbol('off')
14
+
15
+ /** Symbol for adding child to element */
16
+ export const _add = Symbol('init')
17
+
18
+ /** Directive prefix (default: ':') */
19
+ export let prefix = ':';
20
+
21
+ /**
22
+ * A reactive signal containing a value.
23
+ * @template T
24
+ * @typedef {Object} Signal
25
+ * @property {T} value - Current value (reading subscribes, writing notifies)
26
+ * @property {() => T} peek - Read without subscribing
27
+ * @property {() => T} valueOf - Get value for coercion
28
+ * @property {() => T} toJSON - Get value for JSON serialization
29
+ * @property {() => string} toString - Get value as string
30
+ */
31
+
32
+ /**
33
+ * Internal effect function type.
34
+ * @typedef {Object} EffectFn
35
+ * @property {Set<Set<EffectFn>>} deps - Dependency sets
36
+ * @property {() => void} fn - Original function
37
+ */
38
+
39
+ /**
40
+ * Creates a reactive signal.
41
+ * @template T
42
+ * @type {<T>(value: T) => Signal<T>}
43
+ */
44
+ export let signal;
45
+
46
+ /**
47
+ * Creates a reactive effect that re-runs when dependencies change.
48
+ * @type {(fn: () => void | (() => void)) => () => void}
49
+ */
50
+ export let effect;
51
+
52
+ /**
53
+ * Creates a computed signal derived from other signals.
54
+ * @template T
55
+ * @type {<T>(fn: () => T) => Signal<T>}
56
+ */
57
+ export let computed;
58
+
59
+ /**
60
+ * Batches multiple signal updates into a single notification.
61
+ * @template T
62
+ * @type {<T>(fn: () => T) => T}
63
+ */
64
+ export let batch = (fn) => fn();
65
+
66
+ /**
67
+ * Runs a function without tracking signal dependencies.
68
+ * @template T
69
+ * @type {<T>(fn: () => T) => T}
70
+ */
71
+ export let untracked = batch;
72
+
73
+ /**
74
+ * Registry of directive handlers.
75
+ * @type {Record<string, DirectiveHandler>}
76
+ */
77
+ export let directive = {};
78
+
79
+ /**
80
+ * Registry of modifier functions.
81
+ * @type {Record<string, ModifierHandler>}
82
+ */
83
+ export let modifier = {}
12
84
 
13
85
  let currentDir = null;
86
+ let currentEl = null;
87
+
88
+ /**
89
+ * Formats element for error message (minimal context).
90
+ * @param {Element} [el] - Element to format
91
+ * @returns {string} Element hint like "<div#id.class>"
92
+ */
93
+ const elHint = (el) => {
94
+ if (!el?.tagName) return ''
95
+ let hint = el.tagName.toLowerCase()
96
+ if (el.id) hint += '#' + el.id
97
+ else if (el.className) hint += '.' + el.className.split(' ')[0]
98
+ return `<${hint}>`
99
+ }
100
+
101
+ /**
102
+ * Reports an error with context.
103
+ * @param {Error|string} e - Error to report
104
+ * @param {string} [expr] - Expression that caused error
105
+ */
106
+ const err = (e, expr) => {
107
+ let msg = `∴ ${e}`
108
+ if (currentEl) msg += `\n in ${elHint(currentEl)}`
109
+ if (currentDir && expr) msg += `\n ${currentDir}="${expr}"`
110
+ console.error(msg)
111
+ }
112
+
113
+ /**
114
+ * @callback DirectiveHandler
115
+ * @param {Element} el - Target element
116
+ * @param {Object} state - Reactive state object
117
+ * @param {string} expr - Expression string
118
+ * @param {string} [name] - Directive name with modifiers
119
+ * @returns {((value: any) => void | (() => void)) | { [Symbol.dispose]: () => void } | void}
120
+ */
121
+
122
+ /**
123
+ * @callback ModifierHandler
124
+ * @param {Function} fn - Function to modify
125
+ * @param {...string} args - Modifier arguments (from dash-separated values)
126
+ * @returns {Function}
127
+ */
128
+
129
+ /**
130
+ * @typedef {Object} SpraeState
131
+ * @property {Record<string, Signal>} [_signals] - Internal signals map
132
+ */
14
133
 
15
134
  /**
16
135
  * Applies directives to an HTML element and manages its reactive state.
17
136
  *
18
137
  * @param {Element} [el=document.body] - The target HTML element to apply directives to.
19
- * @param {Object|store} [state] - Initial state values to populate the element's reactive state.
20
- * @returns {Object} The reactive state object associated with the element.
138
+ * @param {Object} [state] - Initial state values to populate the element's reactive state.
139
+ * @returns {SpraeState & Object} The reactive state object associated with the element.
21
140
  */
22
141
  const sprae = (el = document.body, state) => {
23
142
  // repeated call can be caused by eg. :each with new objects with old keys
@@ -50,6 +169,7 @@ const sprae = (el = document.body, state) => {
50
169
  el.removeAttribute(name)
51
170
 
52
171
  currentDir = name;
172
+ currentEl = el;
53
173
 
54
174
  // directive initializer can be redefined
55
175
  fx.push(start = dir(el, name.slice(prefix.length), value, state)), offs.push(start())
@@ -60,8 +180,12 @@ const sprae = (el = document.body, state) => {
60
180
  }
61
181
 
62
182
  // :if and :each replace element with text node, which tweaks .children length, but .childNodes length persists
63
- // for (let i = 0, child; i < (el.childNodes.length); i++) child = el.childNodes[i], child.nodeType == 1 && add(child)
64
- for (let child of [...el.childNodes]) child.nodeType == 1 && add(child)
183
+ // real DOM: firstChild/nextSibling avoids array copy; frag.childNodes is already snapshot array
184
+ if (el.firstChild !== undefined) {
185
+ let child = el.firstChild, next
186
+ while (child) (next = child.nextSibling, child.nodeType == 1 && add(child), child = next)
187
+ }
188
+ else for (let child of el.childNodes) child.nodeType == 1 && add(child)
65
189
  };
66
190
 
67
191
  add(el);
@@ -75,80 +199,82 @@ const sprae = (el = document.body, state) => {
75
199
  }
76
200
 
77
201
  // directive initializer
202
+ /** @type {(el: Element, name: string, expr: string, state: Object) => () => (() => void) | void} */
78
203
  export let dir
79
204
 
80
205
  /**
81
- * Compiles an expression into an evaluator function.
82
- * @type {(dir:string, expr: string, clean?: string => string) => Function}
206
+ * Compiles an expression string into an evaluator function.
207
+ * @type {(expr: string) => (state: Object) => any}
83
208
  */
84
209
  export let compile
85
210
 
86
211
  /**
87
212
  * Parses an expression into an evaluator function, caching the result for reuse.
88
213
  *
89
- * @param {string} expr The expression to parse and compile into a function.
90
- * @returns {Function} The compiled evaluator function for the expression.
214
+ * @param {string} expr - The expression to parse and compile into a function.
215
+ * @returns {(state: Object, cb?: (value: any) => any) => any} The compiled evaluator function for the expression.
91
216
  */
92
217
  export const parse = (expr) => {
93
218
  let fn = cache[expr=expr.trim()]
94
219
  if (fn) return fn
95
220
 
96
- let _expr = (expr || 'undefined') + '\n'
97
-
98
- // if, const, let - no return
99
- if (/^(if|let|const)\b/.test(_expr));
100
- // first-level semicolons - no return
101
- else if (hasSemi(_expr));
102
- else _expr = `return ${_expr}`
103
-
104
- // async expression
105
- if (/\bawait\s/.test(_expr)) _expr = `return (async()=>{${_expr}})()`
106
-
107
221
  // static time errors
108
222
  try {
109
- fn = compile(_expr)
223
+ fn = compile(expr || 'undefined')
110
224
  // Object.defineProperty(fn, "name", { value: `∴ ${expr}` })
111
- } catch (e) { console.error(`∴ ${e}\n\n${currentDir}="${expr}"`) }
225
+ } catch (e) { err(e, expr) }
112
226
 
113
227
  // run time errors
114
228
  return cache[expr] = function (state, cb, _out) {
115
229
  try {
116
230
  let result = fn?.call(this, state)
117
231
  // if cb is given (to handle async/await exprs, usually directive update) - call it with result and return a cleanup function
118
- if (cb) return result?.then ? (result.then(v => _out = cb(v)), () => typeof _out === 'function' && _out()) : cb(result)
232
+ if (cb) return result?.then
233
+ ? (result.then(v => _out = cb(v)).catch(e => err(e, expr)), () => typeof _out === 'function' && _out())
234
+ : cb(result)
119
235
  else return result
120
236
  } catch (e) {
121
- console.error(`∴ ${e}\n\n${currentDir}="${expr}"`)
237
+ err(e, expr)
122
238
  }
123
239
  }
124
240
  }
125
241
  const cache = {};
126
242
 
127
- const hasSemi = s => {
128
- for (let d=0,i=0;i<s.length;i++) {
129
- if (s[i]=='{') d++
130
- else if (s[i]=='}') d--
131
- else if (s[i]==';' && !d) return true
132
- }
133
- return false
134
- }
135
243
 
244
+ /**
245
+ * @typedef {Object} SpraeConfig
246
+ * @property {(expr: string) => (state: Object) => any} [compile] - Custom expression compiler
247
+ * @property {string} [prefix] - Directive prefix (default: ':')
248
+ * @property {<T>(value: T) => Signal<T>} [signal] - Signal factory
249
+ * @property {(fn: () => void | (() => void)) => () => void} [effect] - Effect factory
250
+ * @property {<T>(fn: () => T) => Signal<T>} [computed] - Computed factory
251
+ * @property {<T>(fn: () => T) => T} [batch] - Batch function
252
+ * @property {<T>(fn: () => T) => T} [untracked] - Untracked function
253
+ * @property {(el: Element, name: string, expr: string, state: Object) => () => (() => void) | void} [dir] - Directive initializer
254
+ */
136
255
 
137
256
  /**
138
- * Configure sprae
257
+ * Configure sprae with custom signals, compiler, or prefix.
258
+ * @param {SpraeConfig} config - Configuration options
259
+ * @returns {void}
139
260
  */
140
- export const use = (s) => (
141
- s.compile && (compile = s.compile),
142
- s.prefix && (prefix = s.prefix),
143
- s.signal && (signal = s.signal),
144
- s.effect && (effect = s.effect),
145
- s.computed && (computed = s.computed),
146
- s.batch && (batch = s.batch),
147
- s.untracked && (untracked = s.untracked),
148
- s.dir && (dir = s.dir)
261
+ export const use = (config) => (
262
+ config.compile && (compile = config.compile),
263
+ config.prefix && (prefix = config.prefix),
264
+ config.signal && (signal = config.signal),
265
+ config.effect && (effect = config.effect),
266
+ config.computed && (computed = config.computed),
267
+ config.batch && (batch = config.batch),
268
+ config.untracked && (untracked = config.untracked),
269
+ config.dir && (dir = config.dir)
149
270
  )
150
271
 
151
- // modifier applier
272
+ /**
273
+ * Applies modifiers to a function.
274
+ * @param {Function & { target?: Element }} fn - Function to decorate
275
+ * @param {string[]} mods - Modifier names with arguments (e.g., ['throttle-500', 'prevent'])
276
+ * @returns {Function} Decorated function
277
+ */
152
278
  export const decorate = (fn, mods) => {
153
279
  while (mods.length) {
154
280
  let [name, ...params] = mods.pop().split('-'), mod = modifier[name], wrapFn
@@ -162,7 +288,21 @@ export const decorate = (fn, mods) => {
162
288
  return fn
163
289
  }
164
290
 
165
- // instantiated <template> fragment holder, like persisting fragment but with minimal API surface
291
+ /**
292
+ * @typedef {Object} FragmentLike
293
+ * @property {Node[]} childNodes - Child nodes of the fragment
294
+ * @property {DocumentFragment} content - The document fragment content
295
+ * @property {() => void} remove - Remove the fragment from DOM
296
+ * @property {(el: Node) => void} replaceWith - Replace the fragment with an element
297
+ * @property {Attr[]} attributes - Attributes from the original template
298
+ * @property {(name: string) => void} removeAttribute - Remove an attribute
299
+ */
300
+
301
+ /**
302
+ * Creates a fragment holder from a template element with minimal API surface.
303
+ * @param {HTMLTemplateElement | FragmentLike} tpl - Template element or existing fragment
304
+ * @returns {FragmentLike} Fragment-like object
305
+ */
166
306
  export const frag = (tpl) => {
167
307
  if (!tpl.nodeType) return tpl // existing tpl
168
308
 
@@ -173,7 +313,6 @@ export const frag = (tpl) => {
173
313
  childNodes = (content.append(ref), [...content.childNodes])
174
314
 
175
315
  return {
176
- // get parentNode() { return childNodes[0].parentNode },
177
316
  childNodes,
178
317
  content,
179
318
  remove: () => content.append(...childNodes),
@@ -184,25 +323,45 @@ export const frag = (tpl) => {
184
323
  },
185
324
  attributes,
186
325
  removeAttribute(name) { attributes.splice(attributes.findIndex(a => a.name === name), 1) },
187
- // setAttributeNode() { }
188
326
  }
189
327
  }
190
328
 
191
- // camel to kebab
329
+ /**
330
+ * Converts camelCase to kebab-case.
331
+ * @param {string} str - String to convert
332
+ * @returns {string} Kebab-case string
333
+ */
192
334
  export const dashcase = (str) => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match, i) => (i ? '-' : '') + match.toLowerCase());
193
335
 
194
- // set attr
336
+ /**
337
+ * Sets or removes an attribute on an element.
338
+ * @param {Element} el - Target element
339
+ * @param {string} name - Attribute name
340
+ * @param {string | boolean | null | undefined} v - Attribute value (null/false removes, true sets empty)
341
+ * @returns {void}
342
+ */
195
343
  export const attr = (el, name, v) => (v == null || v === false) ? el.removeAttribute(name) : el.setAttribute(name, v === true ? "" : v);
196
344
 
197
- // convert any-arg to className string
345
+ /**
346
+ * Converts class input to className string (like clsx/classnames).
347
+ * @param {string | string[] | Record<string, boolean> | null | undefined} c - Class input
348
+ * @returns {string} Space-separated class string
349
+ */
198
350
  export const clsx = (c, _out = []) => !c ? '' : typeof c === 'string' ? c : (
199
351
  Array.isArray(c) ? c.map(clsx) :
200
352
  Object.entries(c).reduce((s, [k, v]) => !v ? s : [...s, k], [])
201
353
  ).join(' ')
202
354
 
203
- // throttle function to (once per tick or other custom scheduler)
204
- export const throttle = (fn, schedule = queueMicrotask) => {
205
- let _planned = 0, arg;
355
+ /**
356
+ * Throttles a function to run at most once per tick (or custom scheduler).
357
+ * Fires on leading edge, then on trailing edge if called during throttle.
358
+ * @template {Function} T
359
+ * @param {T} fn - Function to throttle
360
+ * @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
361
+ * @returns {T} Throttled function
362
+ */
363
+ export const throttle = (fn, ms) => {
364
+ let _planned = 0, arg, schedule = typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask;
206
365
  const throttled = (e) => {
207
366
  arg = e
208
367
  if (!_planned++) fn(arg), schedule((_dirty = _planned > 1) => (
@@ -212,8 +371,26 @@ export const throttle = (fn, schedule = queueMicrotask) => {
212
371
  return throttled;
213
372
  }
214
373
 
215
- export const debounce = (fn, schedule = queueMicrotask, _count = 0) => (arg, _planned = ++_count) => schedule(() => (_planned == _count && fn(arg)))
374
+ /**
375
+ * Debounces a function to run after a delay since the last call.
376
+ * @template {Function} T
377
+ * @param {T} fn - Function to debounce
378
+ * @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
379
+ * @param {boolean} [immediate=false] - Fire on leading edge instead of trailing
380
+ * @returns {T} Debounced function
381
+ */
382
+ export const debounce = (fn, ms, immediate) => {
383
+ let schedule = typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask;
384
+ return immediate
385
+ ? ((_blocked) => (arg) => !_blocked && (fn(arg), _blocked = 1, schedule(() => _blocked = 0)))()
386
+ : ((_count = 0) => (arg, _c = ++_count) => schedule(() => _c == _count && fn(arg)))()
387
+ }
216
388
 
389
+ /**
390
+ * Parses time string to milliseconds. Supports: 100, 100ms, 1s, 1m
391
+ * @param {string|number} t - Time value
392
+ * @returns {number} Milliseconds
393
+ */
217
394
  export * from './store.js';
218
395
 
219
396
  export default sprae
package/directive/_.js CHANGED
@@ -1,3 +1,11 @@
1
1
  import { attr } from "../core.js";
2
2
 
3
+ /**
4
+ * Default attribute directive - sets any attribute value.
5
+ * @param {Element} el - Target element
6
+ * @param {Object} st - State object
7
+ * @param {string} ex - Expression
8
+ * @param {string} name - Attribute name
9
+ * @returns {(v: any) => void} Update function
10
+ */
3
11
  export default (el, st, ex, name) => v => attr(el, name, typeof v === 'function' ? v(el.getAttribute(name)) : v)
@@ -1,5 +1,14 @@
1
1
  import { clsx, decorate } from "../core.js";
2
2
 
3
+ /**
4
+ * Class directive - manages CSS classes reactively.
5
+ * Supports strings, arrays, and objects (like clsx/classnames).
6
+ * @param {Element} el - Target element
7
+ * @param {Object} st - State object
8
+ * @param {string} ex - Expression
9
+ * @param {string} name - Directive name with modifiers
10
+ * @returns {(v: string | string[] | Record<string, boolean>) => void} Update function
11
+ */
3
12
  export default (el, st, ex, name) => {
4
13
  let _cur = new Set, _new
5
14
 
package/directive/each.js CHANGED
@@ -1,5 +1,13 @@
1
1
  import sprae, { store, parse, _state, effect, _change, _signals, frag, throttle, debounce } from "../core.js";
2
2
 
3
+ /**
4
+ * Each directive - renders list items from array/object/number.
5
+ * Syntax: `:each="item in items"` or `:each="(item, idx) of items"`
6
+ * @param {HTMLTemplateElement | Element} tpl - Template element
7
+ * @param {Object} state - State object
8
+ * @param {string} expr - Iterator expression
9
+ * @returns {{ eval: Function, [Symbol.dispose]: () => void }} Directive result
10
+ */
3
11
  export default (tpl, state, expr) => {
4
12
  const [lhs, rhs] = expr.split(/\bin|of\b/)
5
13
 
@@ -11,7 +19,6 @@ export default (tpl, state, expr) => {
11
19
  // we re-create items any time new items are produced
12
20
  let cur, keys, items, prevl = 0
13
21
 
14
- // FIXME: pass items to update instead of global
15
22
  let update = throttle(() => {
16
23
  let i = 0, newItems = items, newl = newItems.length
17
24
 
@@ -32,27 +39,32 @@ export default (tpl, state, expr) => {
32
39
  // update
33
40
  else while (i < prevl) cur[i] = newItems[i++]
34
41
 
42
+ // batch append using DocumentFragment for efficiency
43
+ let batchSize = newl - i
44
+ let batch = batchSize > 1 ? document.createDocumentFragment() : null
45
+ let pending = batch ? [] : null
46
+
35
47
  // append
36
48
  for (; i < newl; i++) {
37
49
  cur[i] = newItems[i]
38
50
 
39
- let idx = i,
40
- // inherited state must be cheaper in terms of memory and faster in terms of performance, compared to creating a proxy store
41
- // subscope = store({
42
- // // NOTE: since we simulate signal, we have to make sure it's actual signal, not fake one
43
- // // FIXME: try to avoid this, we also have issue with wrongly calling dispose in store on delete
44
- // [itemVar]: cur[_signals]?.[idx]?.peek ? cur[_signals]?.[idx] : cur[idx],
45
- // [idxVar]: keys ? keys[idx] : idx
46
- // }, state)
47
- subscope = Object.create(state, {
48
- [itemVar]: { get: () => cur[idx] },
49
- [idxVar]: { value: keys ? keys[idx] : idx }
50
- })
51
-
51
+ let idx = i
52
52
  let el = tpl.content ? frag(tpl) : tpl.cloneNode(true);
53
-
54
- holder.before(el.content || el);
55
- sprae(el, subscope);
53
+ // el.content is DocumentFragment for frag() output, el itself for cloneNode
54
+ let insertNode = el.content || el
55
+
56
+ // collect for batch insert
57
+ if (batch) {
58
+ batch.appendChild(insertNode)
59
+ pending.push([ el, idx ])
60
+ } else {
61
+ holder.before(insertNode)
62
+ let subscope = Object.create(state, {
63
+ [itemVar]: { get: () => cur[idx] },
64
+ [idxVar]: { value: keys ? keys[idx] : idx }
65
+ })
66
+ sprae(el, subscope)
67
+ }
56
68
 
57
69
  // signal/holder disposal removes element
58
70
  let _prev = ((cur[_signals] ||= [])[i] ||= {})[Symbol.dispose]
@@ -60,6 +72,18 @@ export default (tpl, state, expr) => {
60
72
  _prev?.(), el[Symbol.dispose]?.(), el.remove()
61
73
  };
62
74
  }
75
+
76
+ // batch insert all at once, then sprae
77
+ if (batch) {
78
+ holder.before(batch)
79
+ for (let [el, idx] of pending) {
80
+ let subscope = Object.create(state, {
81
+ [itemVar]: { get: () => cur[idx] },
82
+ [idxVar]: { value: keys ? keys[idx] : idx }
83
+ })
84
+ sprae(el, subscope)
85
+ }
86
+ }
63
87
  }
64
88
 
65
89
  prevl = newl
package/directive/else.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import { _on, _off, _state, frag } from '../core.js';
2
2
 
3
-
4
- // NOTE: we can reach :else counterpart whereas prev :else :if is on hold
3
+ /**
4
+ * Else directive - conditional branch following :if.
5
+ * Can be used as `:else` or `:else :if="condition"`.
6
+ * @param {Element | HTMLTemplateElement} el - Element with directive
7
+ * @returns {() => void} Update function
8
+ */
5
9
  export default (el) => {
6
10
  let _el, _prev = el
7
11
 
@@ -1,5 +1,14 @@
1
1
  import { parse, decorate } from "../core.js"
2
2
 
3
+ /**
4
+ * Event directive - attaches event listeners with modifiers.
5
+ * Syntax: `:onclick="handler"` or `:onclick.prevent.stop="handler"`
6
+ * @param {Element} el - Target element
7
+ * @param {Object} state - State object
8
+ * @param {string} expr - Handler expression
9
+ * @param {string} name - Event name with modifiers (e.g., 'onclick.prevent')
10
+ * @returns {{ [Symbol.dispose]: () => void }} Disposal object
11
+ */
3
12
  export default (el, state, expr, name) => {
4
13
  // wrap inline cb into function
5
14
  // if (!/^(?:[\w$]+|\([^()]*\))\s*=>/.test(expr) && !/^function\b/.test(expr)) expr = `()=>{${expr}}`;
package/directive/fx.js CHANGED
@@ -1 +1,6 @@
1
+ /**
2
+ * Effect directive - runs side effects.
3
+ * Calls function result if expression evaluates to a function.
4
+ * @returns {(fn: any) => any} Update function
5
+ */
1
6
  export default () => (fn) => typeof fn === 'function' && fn()
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Hidden directive - toggles the hidden attribute.
3
+ * @param {Element} el - Target element
4
+ * @returns {(value: any) => boolean} Update function
5
+ */
6
+ export default (el) => (value) => el.hidden = !!value
package/directive/html.js CHANGED
@@ -1,7 +1,30 @@
1
- import sprae, { _dispose, frag } from "../core.js"
1
+ import sprae, { _dispose } from "../core.js"
2
2
 
3
- export default (el, state) => (
4
- // <template :html="a"/> or previously initialized template
5
- el.content && el.replaceWith(el = frag(el).childNodes[0]),
6
- v => (v = typeof v === 'function' ? v(el.innerHTML) : v, el.innerHTML = v == null ? "" : v, sprae(el, state), el[_dispose])
7
- )
3
+ /**
4
+ * HTML directive - sets innerHTML and initializes nested directives.
5
+ * Supports templates for fragment insertion.
6
+ * @param {Element | HTMLTemplateElement} el - Target element
7
+ * @param {Object} state - State object
8
+ * @returns {(v: string | ((html: string) => string)) => void | (() => void)} Update function
9
+ */
10
+ export default (el, state) => {
11
+ // <template :html="a"/> - fragment case: use placeholder + range
12
+ if (el.content) {
13
+ let start = document.createTextNode(''),
14
+ end = document.createTextNode(''),
15
+ range = document.createRange()
16
+ el.replaceWith(start, end)
17
+ return v => {
18
+ v = typeof v === 'function' ? v('') : v
19
+ range.setStartAfter(start)
20
+ range.setEndBefore(end)
21
+ range.deleteContents()
22
+ if (v != null && v !== '') {
23
+ let frag = range.createContextualFragment(v)
24
+ sprae(frag, state)
25
+ range.insertNode(frag)
26
+ }
27
+ }
28
+ }
29
+ return v => (v = typeof v === 'function' ? v(el.innerHTML) : v, el.innerHTML = v == null ? "" : v, sprae(el, state), el[_dispose])
30
+ }
package/directive/if.js CHANGED
@@ -1,7 +1,12 @@
1
- // "centralized" version of :if
2
1
  import sprae, { throttle, _on, _off, _state, frag } from '../core.js';
3
2
 
4
- // :if="a"
3
+ /**
4
+ * Conditional directive - shows/hides element based on condition.
5
+ * Works with :else and :else :if for branching.
6
+ * @param {Element | HTMLTemplateElement} el - Target element
7
+ * @param {Object} state - State object
8
+ * @returns {(value: any) => void} Update function
9
+ */
5
10
  export default (el, state) => {
6
11
  let _holder, _el, _match
7
12
 
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Portal directive - teleports element to another container.
3
+ * Value can be selector string, element, or falsy to return home.
4
+ * @param {Element} el - Element to teleport
5
+ * @param {Object} state - State object
6
+ * @param {string} expr - Expression
7
+ * @returns {(value: string | Element | null | false) => void} Update function
8
+ */
9
+ export default (el, state, expr) => {
10
+ const comment = document.createComment(':portal')
11
+ let currentTarget = null
12
+
13
+ // Insert placeholder before element
14
+ el.before(comment)
15
+
16
+ return (value) => {
17
+ // Resolve target: selector string, element, or null/false to return home
18
+ // For selectors, first try within the same root, then document
19
+ const root = el.getRootNode()
20
+ const target = typeof value === 'string'
21
+ ? (root.querySelector?.(value) || document.querySelector(value))
22
+ : value instanceof Element ? value
23
+ : value ? document.body : null
24
+
25
+ // No change needed
26
+ if (target === currentTarget) return
27
+
28
+ if (target) {
29
+ // Move to target
30
+ target.appendChild(el)
31
+ } else {
32
+ // Return home (after placeholder)
33
+ comment.after(el)
34
+ }
35
+
36
+ currentTarget = target
37
+ }
38
+ }
package/directive/ref.js CHANGED
@@ -1,6 +1,14 @@
1
1
  import { parse } from "../core.js"
2
2
  import { setter } from "./value.js"
3
3
 
4
+ /**
5
+ * Ref directive - stores element reference in state.
6
+ * If expression is a function, calls it with element (returns dispose).
7
+ * @param {Element} el - Target element
8
+ * @param {Object} state - State object
9
+ * @param {string} expr - Variable name or function expression
10
+ * @returns {{ [Symbol.dispose]: () => void } | void} Disposal object or void
11
+ */
4
12
  export default (el, state, expr) => {
5
13
  let fn = parse(expr)(state)
6
14