sprae 12.3.9 → 12.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/core.js +240 -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 +36 -8
  24. package/readme.md +39 -54
  25. package/signal.js +41 -3
  26. package/sprae.js +114 -25
  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,146 @@
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) {
110
+ // Truncate long expressions
111
+ const display = expr.length > 100 ? expr.slice(0, 80) + `… (${expr.length} chars)` : expr
112
+ msg += `\n ${currentDir}="${display}"`
113
+ }
114
+ console.error(msg)
115
+ }
116
+
117
+ /**
118
+ * @callback DirectiveHandler
119
+ * @param {Element} el - Target element
120
+ * @param {Object} state - Reactive state object
121
+ * @param {string} expr - Expression string
122
+ * @param {string} [name] - Directive name with modifiers
123
+ * @returns {((value: any) => void | (() => void)) | { [Symbol.dispose]: () => void } | void}
124
+ */
125
+
126
+ /**
127
+ * @callback ModifierHandler
128
+ * @param {Function} fn - Function to modify
129
+ * @param {...string} args - Modifier arguments (from dash-separated values)
130
+ * @returns {Function}
131
+ */
132
+
133
+ /**
134
+ * @typedef {Object} SpraeState
135
+ * @property {Record<string, Signal>} [_signals] - Internal signals map
136
+ */
14
137
 
15
138
  /**
16
139
  * Applies directives to an HTML element and manages its reactive state.
17
140
  *
18
141
  * @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.
142
+ * @param {Object} [state] - Initial state values to populate the element's reactive state.
143
+ * @returns {SpraeState & Object} The reactive state object associated with the element.
21
144
  */
22
145
  const sprae = (el = document.body, state) => {
23
146
  // repeated call can be caused by eg. :each with new objects with old keys
@@ -50,6 +173,7 @@ const sprae = (el = document.body, state) => {
50
173
  el.removeAttribute(name)
51
174
 
52
175
  currentDir = name;
176
+ currentEl = el;
53
177
 
54
178
  // directive initializer can be redefined
55
179
  fx.push(start = dir(el, name.slice(prefix.length), value, state)), offs.push(start())
@@ -60,8 +184,12 @@ const sprae = (el = document.body, state) => {
60
184
  }
61
185
 
62
186
  // :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)
187
+ // real DOM: firstChild/nextSibling avoids array copy; frag.childNodes is already snapshot array
188
+ if (el.firstChild !== undefined) {
189
+ let child = el.firstChild, next
190
+ while (child) (next = child.nextSibling, child.nodeType == 1 && add(child), child = next)
191
+ }
192
+ else for (let child of el.childNodes) child.nodeType == 1 && add(child)
65
193
  };
66
194
 
67
195
  add(el);
@@ -75,80 +203,82 @@ const sprae = (el = document.body, state) => {
75
203
  }
76
204
 
77
205
  // directive initializer
206
+ /** @type {(el: Element, name: string, expr: string, state: Object) => () => (() => void) | void} */
78
207
  export let dir
79
208
 
80
209
  /**
81
- * Compiles an expression into an evaluator function.
82
- * @type {(dir:string, expr: string, clean?: string => string) => Function}
210
+ * Compiles an expression string into an evaluator function.
211
+ * @type {(expr: string) => (state: Object) => any}
83
212
  */
84
213
  export let compile
85
214
 
86
215
  /**
87
216
  * Parses an expression into an evaluator function, caching the result for reuse.
88
217
  *
89
- * @param {string} expr The expression to parse and compile into a function.
90
- * @returns {Function} The compiled evaluator function for the expression.
218
+ * @param {string} expr - The expression to parse and compile into a function.
219
+ * @returns {(state: Object, cb?: (value: any) => any) => any} The compiled evaluator function for the expression.
91
220
  */
92
221
  export const parse = (expr) => {
93
222
  let fn = cache[expr=expr.trim()]
94
223
  if (fn) return fn
95
224
 
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
225
  // static time errors
108
226
  try {
109
- fn = compile(_expr)
227
+ fn = compile(expr || 'undefined')
110
228
  // Object.defineProperty(fn, "name", { value: `∴ ${expr}` })
111
- } catch (e) { console.error(`∴ ${e}\n\n${currentDir}="${expr}"`) }
229
+ } catch (e) { err(e, expr) }
112
230
 
113
231
  // run time errors
114
232
  return cache[expr] = function (state, cb, _out) {
115
233
  try {
116
234
  let result = fn?.call(this, state)
117
235
  // 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)
236
+ if (cb) return result?.then
237
+ ? (result.then(v => _out = cb(v)).catch(e => err(e, expr)), () => typeof _out === 'function' && _out())
238
+ : cb(result)
119
239
  else return result
120
240
  } catch (e) {
121
- console.error(`∴ ${e}\n\n${currentDir}="${expr}"`)
241
+ err(e, expr)
122
242
  }
123
243
  }
124
244
  }
125
245
  const cache = {};
126
246
 
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
247
 
248
+ /**
249
+ * @typedef {Object} SpraeConfig
250
+ * @property {(expr: string) => (state: Object) => any} [compile] - Custom expression compiler
251
+ * @property {string} [prefix] - Directive prefix (default: ':')
252
+ * @property {<T>(value: T) => Signal<T>} [signal] - Signal factory
253
+ * @property {(fn: () => void | (() => void)) => () => void} [effect] - Effect factory
254
+ * @property {<T>(fn: () => T) => Signal<T>} [computed] - Computed factory
255
+ * @property {<T>(fn: () => T) => T} [batch] - Batch function
256
+ * @property {<T>(fn: () => T) => T} [untracked] - Untracked function
257
+ * @property {(el: Element, name: string, expr: string, state: Object) => () => (() => void) | void} [dir] - Directive initializer
258
+ */
136
259
 
137
260
  /**
138
- * Configure sprae
261
+ * Configure sprae with custom signals, compiler, or prefix.
262
+ * @param {SpraeConfig} config - Configuration options
263
+ * @returns {void}
139
264
  */
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)
265
+ export const use = (config) => (
266
+ config.compile && (compile = config.compile),
267
+ config.prefix && (prefix = config.prefix),
268
+ config.signal && (signal = config.signal),
269
+ config.effect && (effect = config.effect),
270
+ config.computed && (computed = config.computed),
271
+ config.batch && (batch = config.batch),
272
+ config.untracked && (untracked = config.untracked),
273
+ config.dir && (dir = config.dir)
149
274
  )
150
275
 
151
- // modifier applier
276
+ /**
277
+ * Applies modifiers to a function.
278
+ * @param {Function & { target?: Element }} fn - Function to decorate
279
+ * @param {string[]} mods - Modifier names with arguments (e.g., ['throttle-500', 'prevent'])
280
+ * @returns {Function} Decorated function
281
+ */
152
282
  export const decorate = (fn, mods) => {
153
283
  while (mods.length) {
154
284
  let [name, ...params] = mods.pop().split('-'), mod = modifier[name], wrapFn
@@ -162,7 +292,21 @@ export const decorate = (fn, mods) => {
162
292
  return fn
163
293
  }
164
294
 
165
- // instantiated <template> fragment holder, like persisting fragment but with minimal API surface
295
+ /**
296
+ * @typedef {Object} FragmentLike
297
+ * @property {Node[]} childNodes - Child nodes of the fragment
298
+ * @property {DocumentFragment} content - The document fragment content
299
+ * @property {() => void} remove - Remove the fragment from DOM
300
+ * @property {(el: Node) => void} replaceWith - Replace the fragment with an element
301
+ * @property {Attr[]} attributes - Attributes from the original template
302
+ * @property {(name: string) => void} removeAttribute - Remove an attribute
303
+ */
304
+
305
+ /**
306
+ * Creates a fragment holder from a template element with minimal API surface.
307
+ * @param {HTMLTemplateElement | FragmentLike} tpl - Template element or existing fragment
308
+ * @returns {FragmentLike} Fragment-like object
309
+ */
166
310
  export const frag = (tpl) => {
167
311
  if (!tpl.nodeType) return tpl // existing tpl
168
312
 
@@ -173,7 +317,6 @@ export const frag = (tpl) => {
173
317
  childNodes = (content.append(ref), [...content.childNodes])
174
318
 
175
319
  return {
176
- // get parentNode() { return childNodes[0].parentNode },
177
320
  childNodes,
178
321
  content,
179
322
  remove: () => content.append(...childNodes),
@@ -184,25 +327,45 @@ export const frag = (tpl) => {
184
327
  },
185
328
  attributes,
186
329
  removeAttribute(name) { attributes.splice(attributes.findIndex(a => a.name === name), 1) },
187
- // setAttributeNode() { }
188
330
  }
189
331
  }
190
332
 
191
- // camel to kebab
333
+ /**
334
+ * Converts camelCase to kebab-case.
335
+ * @param {string} str - String to convert
336
+ * @returns {string} Kebab-case string
337
+ */
192
338
  export const dashcase = (str) => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match, i) => (i ? '-' : '') + match.toLowerCase());
193
339
 
194
- // set attr
340
+ /**
341
+ * Sets or removes an attribute on an element.
342
+ * @param {Element} el - Target element
343
+ * @param {string} name - Attribute name
344
+ * @param {string | boolean | null | undefined} v - Attribute value (null/false removes, true sets empty)
345
+ * @returns {void}
346
+ */
195
347
  export const attr = (el, name, v) => (v == null || v === false) ? el.removeAttribute(name) : el.setAttribute(name, v === true ? "" : v);
196
348
 
197
- // convert any-arg to className string
349
+ /**
350
+ * Converts class input to className string (like clsx/classnames).
351
+ * @param {string | string[] | Record<string, boolean> | null | undefined} c - Class input
352
+ * @returns {string} Space-separated class string
353
+ */
198
354
  export const clsx = (c, _out = []) => !c ? '' : typeof c === 'string' ? c : (
199
355
  Array.isArray(c) ? c.map(clsx) :
200
356
  Object.entries(c).reduce((s, [k, v]) => !v ? s : [...s, k], [])
201
357
  ).join(' ')
202
358
 
203
- // throttle function to (once per tick or other custom scheduler)
204
- export const throttle = (fn, schedule = queueMicrotask) => {
205
- let _planned = 0, arg;
359
+ /**
360
+ * Throttles a function to run at most once per tick (or custom scheduler).
361
+ * Fires on leading edge, then on trailing edge if called during throttle.
362
+ * @template {Function} T
363
+ * @param {T} fn - Function to throttle
364
+ * @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
365
+ * @returns {T} Throttled function
366
+ */
367
+ export const throttle = (fn, ms) => {
368
+ let _planned = 0, arg, schedule = typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask;
206
369
  const throttled = (e) => {
207
370
  arg = e
208
371
  if (!_planned++) fn(arg), schedule((_dirty = _planned > 1) => (
@@ -212,8 +375,26 @@ export const throttle = (fn, schedule = queueMicrotask) => {
212
375
  return throttled;
213
376
  }
214
377
 
215
- export const debounce = (fn, schedule = queueMicrotask, _count = 0) => (arg, _planned = ++_count) => schedule(() => (_planned == _count && fn(arg)))
378
+ /**
379
+ * Debounces a function to run after a delay since the last call.
380
+ * @template {Function} T
381
+ * @param {T} fn - Function to debounce
382
+ * @param {number|Function} [ms] - Delay in ms or scheduler function (default: microtask)
383
+ * @param {boolean} [immediate=false] - Fire on leading edge instead of trailing
384
+ * @returns {T} Debounced function
385
+ */
386
+ export const debounce = (fn, ms, immediate) => {
387
+ let schedule = typeof ms === 'function' ? ms : ms ? (fn) => setTimeout(fn, ms) : queueMicrotask;
388
+ return immediate
389
+ ? ((_blocked) => (arg) => !_blocked && (fn(arg), _blocked = 1, schedule(() => _blocked = 0)))()
390
+ : ((_count = 0) => (arg, _c = ++_count) => schedule(() => _c == _count && fn(arg)))()
391
+ }
216
392
 
393
+ /**
394
+ * Parses time string to milliseconds. Supports: 100, 100ms, 1s, 1m
395
+ * @param {string|number} t - Time value
396
+ * @returns {number} Milliseconds
397
+ */
217
398
  export * from './store.js';
218
399
 
219
400
  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
+ }