@oscarpalmer/abydon 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fragment.mjs CHANGED
@@ -2,10 +2,14 @@
2
2
  import {html as html2} from "@oscarpalmer/toretto/html";
3
3
  import {parse} from "./html";
4
4
  import {mapNodes} from "./node";
5
+ import {removeNodes} from "./helpers/dom";
5
6
 
6
7
  class Fragment {
7
8
  $fragment = true;
8
9
  data;
10
+ get identifier() {
11
+ return this.data.identifier;
12
+ }
9
13
  constructor(strings, expressions) {
10
14
  this.data = {
11
15
  expressions,
@@ -26,20 +30,25 @@ class Fragment {
26
30
  this.data.items.splice(0, this.data.items.length, ...templated.map((node2) => ({
27
31
  nodes: [node2]
28
32
  })));
29
- mapNodes(this.data, this.data.items.flatMap((item) => item.fragment?.get() ?? item.nodes));
33
+ mapNodes(this.data, this.data.items.flatMap((item) => item.fragments?.flatMap((fragment) => fragment.get()) ?? item.nodes));
30
34
  }
31
35
  return [
32
- ...this.data.items.flatMap((item) => item.fragment?.get() ?? item.nodes)
36
+ ...this.data.items.flatMap((item) => item.fragments?.flatMap((fragment) => fragment.get()) ?? item.nodes)
33
37
  ];
34
38
  }
39
+ identify(identifier) {
40
+ this.data.identifier = identifier;
41
+ return this;
42
+ }
35
43
  remove() {
36
44
  const { length } = this.data.items;
37
45
  for (let index = 0;index < length; index += 1) {
38
- const item = this.data.items[index];
39
- item.fragment?.remove();
40
- for (const node2 of item.nodes) {
41
- node2.remove();
46
+ const { fragments, nodes } = this.data.items[index];
47
+ const { length: length2 } = fragments ?? [];
48
+ for (let index2 = 0;index2 < length2; index2 += 1) {
49
+ fragments?.[index2]?.remove();
42
50
  }
51
+ removeNodes(nodes);
43
52
  }
44
53
  this.data.items.splice(0, length);
45
54
  }
@@ -1,6 +1,7 @@
1
1
  // src/helpers/dom.ts
2
2
  import {getString} from "@oscarpalmer/atoms/string";
3
- import {isFragment, isProperNode} from "./index";
3
+ import {isFragment, isProperElement, isProperNode} from "./index";
4
+ import {removeEvents} from "../node/event";
4
5
  function createNodes(value) {
5
6
  if (isFragment(value)) {
6
7
  return value.get();
@@ -10,18 +11,34 @@ function createNodes(value) {
10
11
  }
11
12
  return [new Text(getString(value))];
12
13
  }
14
+ function removeNodes(nodes) {
15
+ sanitiseNodes(nodes);
16
+ const { length } = nodes;
17
+ for (let index = 0;index < length; index += 1) {
18
+ nodes[index].remove();
19
+ }
20
+ }
13
21
  function replaceNodes(from, to) {
22
+ from[0]?.replaceWith(...to);
14
23
  const { length } = from;
24
+ for (let index = 1;index < length; index += 1) {
25
+ from[index].remove();
26
+ }
27
+ }
28
+ function sanitiseNodes(nodes) {
29
+ const { length } = nodes;
15
30
  for (let index = 0;index < length; index += 1) {
16
- const node = from[index];
17
- if (index === 0) {
18
- node.replaceWith(...to);
19
- } else {
20
- node.remove();
31
+ const node = nodes[index];
32
+ if (isProperElement(node)) {
33
+ removeEvents(node);
34
+ }
35
+ if (node.hasChildNodes()) {
36
+ sanitiseNodes([...node.childNodes]);
21
37
  }
22
38
  }
23
39
  }
24
40
  export {
25
41
  replaceNodes,
42
+ removeNodes,
26
43
  createNodes
27
44
  };
@@ -1,4 +1,13 @@
1
1
  // src/helpers/index.ts
2
+ function compareArrays(first, second) {
3
+ const firstIsLarger = first.length > second.length;
4
+ const from = firstIsLarger ? first : second;
5
+ const to = firstIsLarger ? second : first;
6
+ if (!from.filter((key) => to.includes(key)).every((key, index) => to[index] === key)) {
7
+ return "dissimilar";
8
+ }
9
+ return firstIsLarger ? "removed" : "added";
10
+ }
2
11
  function isFragment(value) {
3
12
  return typeof value === "object" && value != null && "$fragment" in value && value.$fragment === true;
4
13
  }
@@ -11,5 +20,6 @@ function isProperNode(value) {
11
20
  export {
12
21
  isProperNode,
13
22
  isProperElement,
14
- isFragment
23
+ isFragment,
24
+ compareArrays
15
25
  };
package/dist/index.js CHANGED
@@ -163,6 +163,19 @@ function html(value3, sanitisation) {
163
163
  var templates = {};
164
164
 
165
165
  // node_modules/@oscarpalmer/sentinel/node_modules/@oscarpalmer/atoms/dist/js/queue.mjs
166
+ function queue(callback) {
167
+ _atomic_queued.add(callback);
168
+ if (_atomic_queued.size > 0) {
169
+ queueMicrotask(() => {
170
+ const callbacks = [..._atomic_queued];
171
+ const { length } = callbacks;
172
+ _atomic_queued.clear();
173
+ for (let index = 0;index < length; index += 1) {
174
+ callbacks[index]();
175
+ }
176
+ });
177
+ }
178
+ }
166
179
  if (globalThis._atomic_queued == null) {
167
180
  const queued = new Set;
168
181
  Object.defineProperty(globalThis, "_atomic_queued", {
@@ -186,6 +199,22 @@ if (globalThis._sentinels == null) {
186
199
  function effect(callback) {
187
200
  return new Effect(callback);
188
201
  }
202
+ function watch(reactive, key) {
203
+ const effect2 = globalThis._sentinels[globalThis._sentinels.length - 1];
204
+ if (effect2 != null) {
205
+ if (key == null) {
206
+ reactive.callbacks.any.add(effect2);
207
+ } else {
208
+ if (!reactive.callbacks.keys.has(key)) {
209
+ reactive.callbacks.keys.add(key);
210
+ reactive.callbacks.values.set(key, new Set);
211
+ }
212
+ reactive.callbacks.values.get(key)?.add(effect2);
213
+ }
214
+ effect2.reactives.add(reactive);
215
+ }
216
+ }
217
+
189
218
  class Effect {
190
219
  $sentinel = "effect";
191
220
  state;
@@ -230,7 +259,97 @@ function isSentinel(value3, expression) {
230
259
  return expression.test(value3?.$sentinel ?? "");
231
260
  }
232
261
 
262
+ // node_modules/@oscarpalmer/sentinel/node_modules/@oscarpalmer/atoms/dist/js/function.mjs
263
+ function noop() {
264
+ }
265
+
266
+ // node_modules/@oscarpalmer/sentinel/dist/helpers/subscription.mjs
267
+ function subscribe(state, subscriber, key) {
268
+ let set5;
269
+ if (key != null) {
270
+ if (state.callbacks.keys.has(key)) {
271
+ set5 = state.callbacks.values.get(key);
272
+ } else {
273
+ set5 = new Set;
274
+ state.callbacks.keys.add(key);
275
+ state.callbacks.values.set(key, set5);
276
+ }
277
+ } else {
278
+ set5 = state.callbacks.any;
279
+ }
280
+ if (set5 == null || set5.has(subscriber)) {
281
+ return noop;
282
+ }
283
+ set5.add(subscriber);
284
+ subscriber(state.value);
285
+ return () => {
286
+ unsubscribe(state, subscriber, key);
287
+ };
288
+ }
289
+ function unsubscribe(state, subscriber, key) {
290
+ if (key != null) {
291
+ const set5 = state.callbacks.values.get(key);
292
+ if (set5 != null) {
293
+ if (subscriber == null) {
294
+ set5.clear();
295
+ } else {
296
+ set5.delete(subscriber);
297
+ }
298
+ if (set5.size === 0) {
299
+ state.callbacks.keys.delete(key);
300
+ state.callbacks.values.delete(key);
301
+ }
302
+ }
303
+ } else if (subscriber != null) {
304
+ state.callbacks.any.delete(subscriber);
305
+ }
306
+ }
307
+
308
+ // node_modules/@oscarpalmer/sentinel/dist/helpers/event.mjs
309
+ function disable(state) {
310
+ if (state.active) {
311
+ state.active = false;
312
+ const effects = [...state.callbacks.any, ...state.callbacks.values.values()].flatMap((value3) => value3 instanceof Set ? [...value3.values()] : value3).filter((value3) => typeof value3 !== "function");
313
+ for (const fx of effects) {
314
+ if (typeof fx !== "function") {
315
+ fx.reactives.delete(state);
316
+ }
317
+ }
318
+ }
319
+ }
320
+ function emit(state, keys) {
321
+ if (state.active) {
322
+ const subscribers = [
323
+ ...state.callbacks.any,
324
+ ...[...state.callbacks.values.entries()].filter(([key]) => keys == null || keys.includes(key)).map(([, value3]) => value3)
325
+ ].flatMap((value3) => value3 instanceof Set ? [...value3.values()] : value3).map((value3) => typeof value3 === "function" ? value3 : value3.callback);
326
+ for (const subsriber of subscribers) {
327
+ if (typeof subsriber === "function") {
328
+ queue(() => {
329
+ subsriber(state.value);
330
+ });
331
+ }
332
+ }
333
+ }
334
+ }
335
+ function enable(state) {
336
+ if (!state.active) {
337
+ state.active = true;
338
+ emit(state);
339
+ }
340
+ }
341
+
233
342
  // node_modules/@oscarpalmer/sentinel/dist/helpers/value.mjs
343
+ function getValue3(reactive, key) {
344
+ watch(reactive, typeof key === "symbol" ? undefined : key);
345
+ return key == null ? reactive.value : Array.isArray(reactive.value) ? reactive.value.at(key) : reactive.value[key];
346
+ }
347
+ function setValue3(reactive, value3) {
348
+ if (!Object.is(reactive.value, value3)) {
349
+ reactive.value = value3;
350
+ emit(reactive);
351
+ }
352
+ }
234
353
  var arrayOperations = new Set([
235
354
  "copyWithin",
236
355
  "fill",
@@ -242,6 +361,74 @@ var arrayOperations = new Set([
242
361
  "splice",
243
362
  "unshift"
244
363
  ]);
364
+
365
+ // node_modules/@oscarpalmer/sentinel/dist/reactive/instance.mjs
366
+ class ReactiveInstance {
367
+ state;
368
+ constructor(type, value22) {
369
+ this.$sentinel = type;
370
+ this.state = {
371
+ value: value22,
372
+ active: true,
373
+ callbacks: {
374
+ any: new Set,
375
+ keys: new Set,
376
+ values: new Map
377
+ }
378
+ };
379
+ }
380
+ get() {
381
+ return getValue3(this.state);
382
+ }
383
+ peek() {
384
+ return this.state.value;
385
+ }
386
+ run() {
387
+ enable(this.state);
388
+ }
389
+ stop() {
390
+ disable(this.state);
391
+ }
392
+ toJSON() {
393
+ return getValue3(this.state);
394
+ }
395
+ toString() {
396
+ return String(getValue3(this.state));
397
+ }
398
+ }
399
+
400
+ // node_modules/@oscarpalmer/sentinel/dist/reactive/value.mjs
401
+ class ReactiveValue extends ReactiveInstance {
402
+ subscribe(subscriber) {
403
+ return subscribe(this.state, subscriber);
404
+ }
405
+ unsubscribe(subscriber) {
406
+ if (subscriber == null) {
407
+ this.state.callbacks.any.clear();
408
+ } else {
409
+ this.state.callbacks.any.delete(subscriber);
410
+ }
411
+ }
412
+ }
413
+
414
+ // node_modules/@oscarpalmer/sentinel/dist/reactive/computed.mjs
415
+ function computed(value32) {
416
+ return new Computed(value32);
417
+ }
418
+
419
+ class Computed extends ReactiveValue {
420
+ fx;
421
+ constructor(value32) {
422
+ super("computed", undefined);
423
+ this.fx = effect(() => setValue3(this.state, value32()));
424
+ }
425
+ run() {
426
+ this.fx.start();
427
+ }
428
+ stop() {
429
+ this.fx.stop();
430
+ }
431
+ }
245
432
  // node_modules/@oscarpalmer/sentinel/dist/reactive/index.mjs
246
433
  var primitives = new Set(["boolean", "number", "string"]);
247
434
 
@@ -256,6 +443,42 @@ function isProperNode(value11) {
256
443
  return value11 instanceof CharacterData || value11 instanceof Element;
257
444
  }
258
445
 
446
+ // src/node/event.ts
447
+ function getController(element) {
448
+ let controller = controllers.get(element);
449
+ if (controller == null) {
450
+ controller = new AbortController;
451
+ controllers.set(element, controller);
452
+ }
453
+ return controller;
454
+ }
455
+ function getOptions(options) {
456
+ const parts = options.split(":");
457
+ return {
458
+ capture: parts.includes("c") || parts.includes("capture"),
459
+ once: parts.includes("o") || parts.includes("once"),
460
+ passive: !parts.includes("a") && !parts.includes("active")
461
+ };
462
+ }
463
+ function mapEvent(element, name, value11) {
464
+ element.removeAttribute(name);
465
+ const [, type, options] = eventNamePattern.exec(name) ?? [];
466
+ if (typeof value11 === "function" && type != null) {
467
+ element.addEventListener(type, value11, {
468
+ ...getOptions(options ?? ""),
469
+ signal: getController(element).signal
470
+ });
471
+ }
472
+ }
473
+ function removeEvents(element) {
474
+ if (controllers.has(element)) {
475
+ controllers.get(element)?.abort();
476
+ controllers.delete(element);
477
+ }
478
+ }
479
+ var controllers = new WeakMap;
480
+ var eventNamePattern = /^@([\w-]+)(?::([a-z:]+))?$/i;
481
+
259
482
  // src/helpers/dom.ts
260
483
  function createNodes(value11) {
261
484
  if (isFragment(value11)) {
@@ -266,32 +489,33 @@ function createNodes(value11) {
266
489
  }
267
490
  return [new Text(getString(value11))];
268
491
  }
492
+ function removeNodes(nodes) {
493
+ sanitiseNodes2(nodes);
494
+ const { length } = nodes;
495
+ for (let index = 0;index < length; index += 1) {
496
+ nodes[index].remove();
497
+ }
498
+ }
269
499
  function replaceNodes(from, to) {
270
500
  const { length } = from;
271
501
  for (let index = 0;index < length; index += 1) {
272
- const node = from[index];
273
502
  if (index === 0) {
274
- node.replaceWith(...to);
503
+ from[index].replaceWith(...to);
275
504
  } else {
276
- node.remove();
505
+ from[index].remove();
277
506
  }
278
507
  }
279
508
  }
280
-
281
- // src/node/event.ts
282
- function getOptions(options) {
283
- const parts = options.split(":");
284
- return {
285
- capture: parts.includes("c") || parts.includes("capture"),
286
- once: parts.includes("o") || parts.includes("once"),
287
- passive: !parts.includes("a") && !parts.includes("active")
288
- };
289
- }
290
- function mapEvent(element, name, value11) {
291
- element.removeAttribute(name);
292
- const [, type, options] = /^@(\w+)(?::([a-z:]+))?$/.exec(name) ?? [];
293
- if (typeof value11 === "function" && type != null) {
294
- element.addEventListener(type, value11, getOptions(options ?? ""));
509
+ function sanitiseNodes2(nodes) {
510
+ const { length } = nodes;
511
+ for (let index = 0;index < length; index += 1) {
512
+ const node = nodes[index];
513
+ if (isProperElement(node)) {
514
+ removeEvents(node);
515
+ }
516
+ if (node.hasChildNodes()) {
517
+ sanitiseNodes2([...node.childNodes]);
518
+ }
295
519
  }
296
520
  }
297
521
 
@@ -299,10 +523,10 @@ function mapEvent(element, name, value11) {
299
523
  function setAttribute2(element, name, value11) {
300
524
  element.removeAttribute(name);
301
525
  switch (true) {
302
- case classPattern.test(name):
526
+ case classExpression.test(name):
303
527
  setClasses(element, name, value11);
304
528
  return;
305
- case stylePattern.test(name):
529
+ case stylePrefixExpression.test(name):
306
530
  setStyle(element, name, value11);
307
531
  return;
308
532
  default:
@@ -312,7 +536,7 @@ function setAttribute2(element, name, value11) {
312
536
  }
313
537
  function setClasses(element, name, value11) {
314
538
  function update(value12) {
315
- if (/^(|true)$/.test(getString(value12))) {
539
+ if (value12 === true) {
316
540
  element.classList.add(...classes);
317
541
  } else {
318
542
  element.classList.remove(...classes);
@@ -335,16 +559,15 @@ function setStyle(element, name, value11) {
335
559
  element.style.setProperty(property, value12 === true ? unit : getString(value12));
336
560
  }
337
561
  }
338
- const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
339
- if (property == null) {
340
- return;
341
- }
342
- if (isReactive(value11)) {
343
- effect(() => {
344
- update(value11.get());
345
- });
346
- } else {
347
- update(value11);
562
+ const [, property, unit] = styleFullExpression.exec(name) ?? [];
563
+ if (property != null) {
564
+ if (isReactive(value11)) {
565
+ effect(() => {
566
+ update(value11.get());
567
+ });
568
+ } else {
569
+ update(value11);
570
+ }
348
571
  }
349
572
  }
350
573
  function setValue5(element, name, value11) {
@@ -363,7 +586,7 @@ function triggerSelectChange(select) {
363
586
  }
364
587
  }
365
588
  function updateProperty(element, name, value11) {
366
- element[name] = /^(|true)$/.test(getString(value11));
589
+ element[name] = value11 === true;
367
590
  }
368
591
  function updateSelected(element, name, value11) {
369
592
  const select = element.closest("select");
@@ -373,8 +596,9 @@ function updateSelected(element, name, value11) {
373
596
  }
374
597
  updateProperty(element, name, value11);
375
598
  }
376
- var classPattern = /^class\./;
377
- var stylePattern = /^style\./;
599
+ var classExpression = /^class\./;
600
+ var styleFullExpression = /^style\.([\w-]+)(?:\.([\w-]+))?$/;
601
+ var stylePrefixExpression = /^style\./;
378
602
 
379
603
  // src/node/attribute/index.ts
380
604
  function getValue5(data, original) {
@@ -389,7 +613,7 @@ function mapAttributes(data, element) {
389
613
  const actual = getValue5(data, value12);
390
614
  if (name.startsWith("@")) {
391
615
  mapEvent(element, name, actual);
392
- } else if (name.includes(".") || isReactive(actual)) {
616
+ } else if (name.includes(".") || typeof value12 === "function" || isReactive(actual)) {
393
617
  mapValue(element, name, actual);
394
618
  }
395
619
  }
@@ -397,8 +621,8 @@ function mapAttributes(data, element) {
397
621
  function mapValue(element, name, value12) {
398
622
  switch (true) {
399
623
  case typeof value12 === "function":
400
- mapValue(element, name, value12());
401
- return;
624
+ setAttribute2(element, name, computed(value12));
625
+ break;
402
626
  default:
403
627
  setAttribute2(element, name, value12);
404
628
  break;
@@ -483,8 +707,8 @@ function mapNodes(data, nodes) {
483
707
  function mapValue2(data, comment, value13) {
484
708
  switch (true) {
485
709
  case typeof value13 === "function":
486
- mapValue2(data, comment, value13());
487
- return;
710
+ setReactiveNode(data, comment, computed(value13));
711
+ break;
488
712
  case isReactive(value13):
489
713
  setReactiveNode(data, comment, value13);
490
714
  break;
@@ -537,11 +761,9 @@ class Fragment {
537
761
  remove() {
538
762
  const { length } = this.data.items;
539
763
  for (let index = 0;index < length; index += 1) {
540
- const item = this.data.items[index];
541
- item.fragment?.remove();
542
- for (const node2 of item.nodes) {
543
- node2.remove();
544
- }
764
+ const { fragment, nodes } = this.data.items[index];
765
+ fragment?.remove();
766
+ removeNodes(nodes);
545
767
  }
546
768
  this.data.items.splice(0, length);
547
769
  }
@@ -1,5 +1,5 @@
1
1
  // src/node/attribute/index.ts
2
- import {isReactive} from "@oscarpalmer/sentinel";
2
+ import {computed, isReactive} from "@oscarpalmer/sentinel";
3
3
  import {mapEvent} from "../event";
4
4
  import {setAttribute} from "./value";
5
5
  function getValue(data, original) {
@@ -14,7 +14,7 @@ function mapAttributes(data, element) {
14
14
  const actual = getValue(data, value2);
15
15
  if (name.startsWith("@")) {
16
16
  mapEvent(element, name, actual);
17
- } else if (name.includes(".") || isReactive(actual)) {
17
+ } else if (name.includes(".") || typeof value2 === "function" || isReactive(actual)) {
18
18
  mapValue(element, name, actual);
19
19
  }
20
20
  }
@@ -22,8 +22,8 @@ function mapAttributes(data, element) {
22
22
  function mapValue(element, name, value2) {
23
23
  switch (true) {
24
24
  case typeof value2 === "function":
25
- mapValue(element, name, value2());
26
- return;
25
+ setAttribute(element, name, computed(value2));
26
+ break;
27
27
  default:
28
28
  setAttribute(element, name, value2);
29
29
  break;
@@ -8,10 +8,10 @@ setAttribute as setAttr
8
8
  function setAttribute(element, name, value) {
9
9
  element.removeAttribute(name);
10
10
  switch (true) {
11
- case classPattern.test(name):
11
+ case classExpression.test(name):
12
12
  setClasses(element, name, value);
13
13
  return;
14
- case stylePattern.test(name):
14
+ case stylePrefixExpression.test(name):
15
15
  setStyle(element, name, value);
16
16
  return;
17
17
  default:
@@ -21,7 +21,7 @@ function setAttribute(element, name, value) {
21
21
  }
22
22
  function setClasses(element, name, value) {
23
23
  function update(value2) {
24
- if (/^(|true)$/.test(getString(value2))) {
24
+ if (value2 === true) {
25
25
  element.classList.add(...classes);
26
26
  } else {
27
27
  element.classList.remove(...classes);
@@ -44,16 +44,15 @@ function setStyle(element, name, value) {
44
44
  element.style.setProperty(property, value2 === true ? unit : getString(value2));
45
45
  }
46
46
  }
47
- const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
48
- if (property == null) {
49
- return;
50
- }
51
- if (isReactive(value)) {
52
- effect(() => {
53
- update(value.get());
54
- });
55
- } else {
56
- update(value);
47
+ const [, property, unit] = styleFullExpression.exec(name) ?? [];
48
+ if (property != null) {
49
+ if (isReactive(value)) {
50
+ effect(() => {
51
+ update(value.get());
52
+ });
53
+ } else {
54
+ update(value);
55
+ }
57
56
  }
58
57
  }
59
58
  function setValue(element, name, value) {
@@ -72,7 +71,7 @@ function triggerSelectChange(select) {
72
71
  }
73
72
  }
74
73
  function updateProperty(element, name, value) {
75
- element[name] = /^(|true)$/.test(getString(value));
74
+ element[name] = value === true;
76
75
  }
77
76
  function updateSelected(element, name, value) {
78
77
  const select = element.closest("select");
@@ -82,8 +81,9 @@ function updateSelected(element, name, value) {
82
81
  }
83
82
  updateProperty(element, name, value);
84
83
  }
85
- var classPattern = /^class\./;
86
- var stylePattern = /^style\./;
84
+ var classExpression = /^class\./;
85
+ var styleFullExpression = /^style\.([\w-]+)(?:\.([\w-]+))?$/;
86
+ var stylePrefixExpression = /^style\./;
87
87
  export {
88
88
  setAttribute
89
89
  };
@@ -1,4 +1,12 @@
1
1
  // src/node/event.ts
2
+ function getController(element) {
3
+ let controller = controllers.get(element);
4
+ if (controller == null) {
5
+ controller = new AbortController;
6
+ controllers.set(element, controller);
7
+ }
8
+ return controller;
9
+ }
2
10
  function getOptions(options) {
3
11
  const parts = options.split(":");
4
12
  return {
@@ -9,11 +17,23 @@ function getOptions(options) {
9
17
  }
10
18
  function mapEvent(element, name, value) {
11
19
  element.removeAttribute(name);
12
- const [, type, options] = /^@(\w+)(?::([a-z:]+))?$/.exec(name) ?? [];
20
+ const [, type, options] = eventNamePattern.exec(name) ?? [];
13
21
  if (typeof value === "function" && type != null) {
14
- element.addEventListener(type, value, getOptions(options ?? ""));
22
+ element.addEventListener(type, value, {
23
+ ...getOptions(options ?? ""),
24
+ signal: getController(element).signal
25
+ });
26
+ }
27
+ }
28
+ function removeEvents(element) {
29
+ if (controllers.has(element)) {
30
+ controllers.get(element)?.abort();
31
+ controllers.delete(element);
15
32
  }
16
33
  }
34
+ var controllers = new WeakMap;
35
+ var eventNamePattern = /^@([\w-]+)(?::([a-z:]+))?$/i;
17
36
  export {
37
+ removeEvents,
18
38
  mapEvent
19
39
  };
@@ -1,5 +1,5 @@
1
1
  // src/node/index.ts
2
- import {isReactive} from "@oscarpalmer/sentinel";
2
+ import {computed, isReactive} from "@oscarpalmer/sentinel";
3
3
  import {isFragment, isProperElement} from "../helpers";
4
4
  import {createNodes} from "../helpers/dom";
5
5
  import {mapAttributes} from "./attribute";
@@ -30,8 +30,8 @@ function mapNodes(data, nodes) {
30
30
  function mapValue(data, comment, value2) {
31
31
  switch (true) {
32
32
  case typeof value2 === "function":
33
- mapValue(data, comment, value2());
34
- return;
33
+ setReactiveNode(data, comment, computed(value2));
34
+ break;
35
35
  case isReactive(value2):
36
36
  setReactiveNode(data, comment, value2);
37
37
  break;
@@ -44,7 +44,7 @@ function replaceComment(data, comment, value2) {
44
44
  const item = data.items.find((item2) => item2.nodes.includes(comment));
45
45
  const nodes = createNodes(value2);
46
46
  if (item != null) {
47
- item.fragment = isFragment(value2) ? value2 : undefined;
47
+ item.fragments = isFragment(value2) ? [value2] : undefined;
48
48
  item.nodes = nodes;
49
49
  }
50
50
  comment.replaceWith(...nodes);
@@ -2,10 +2,68 @@
2
2
  import {isNullableOrWhitespace} from "@oscarpalmer/atoms/is";
3
3
  import {getString} from "@oscarpalmer/atoms/string";
4
4
  import {effect} from "@oscarpalmer/sentinel";
5
- import {isFragment, isProperNode} from "../helpers";
5
+ import {compareArrays, isFragment, isProperNode} from "../helpers";
6
6
  import {createNodes, replaceNodes} from "../helpers/dom";
7
- function setNodes(nodes, comment, text, value) {
8
- const next = createNodes(value);
7
+ function removeFragments(fragments) {
8
+ if (fragments != null) {
9
+ const { length } = fragments;
10
+ for (let index = 0;index < length; index += 1) {
11
+ fragments[index].remove();
12
+ }
13
+ }
14
+ }
15
+ function setArray(fragments, nodes, comment, text, value) {
16
+ if (value.length === 0) {
17
+ return {
18
+ nodes: setText(fragments, nodes, comment, text, value)
19
+ };
20
+ }
21
+ let templates = value.filter((item) => isFragment(item) && item.identifier != null);
22
+ const identifiers = templates.map((fragment) => fragment.identifier);
23
+ const oldIdentifiers = fragments?.map((fragment) => fragment.identifier) ?? [];
24
+ if (new Set(identifiers).size !== templates.length) {
25
+ templates = [];
26
+ }
27
+ const noTemplates = templates.length === 0;
28
+ if (noTemplates || nodes == null || oldIdentifiers.some((identifier) => identifier == null)) {
29
+ return {
30
+ fragments: noTemplates ? undefined : templates,
31
+ nodes: setNodes(fragments, nodes, comment, text, noTemplates ? value.flatMap((item) => createNodes(item)) : templates.flatMap((template) => template.get()))
32
+ };
33
+ }
34
+ const next = templates.map((template) => fragments?.find((fragment) => fragment.identifier === template.identifier) ?? template);
35
+ const comparison = compareArrays(fragments ?? [], templates);
36
+ if (comparison !== "removed") {
37
+ let position = nodes[0];
38
+ const before = comparison === "added" && !oldIdentifiers.includes(templates[0].identifier);
39
+ const items = next.flatMap((fragment) => fragment.get().flatMap((node) => ({
40
+ identifier: fragment.identifier,
41
+ value: node
42
+ })));
43
+ const { length: length2 } = items;
44
+ for (let index = 0;index < length2; index += 1) {
45
+ const item = items[index];
46
+ if (comparison === "dissimilar" || !oldIdentifiers.includes(item.identifier)) {
47
+ if (index === 0 && before) {
48
+ position.before(item.value);
49
+ } else {
50
+ position.after(item.value);
51
+ }
52
+ }
53
+ position = item.value;
54
+ }
55
+ }
56
+ const toRemove = fragments?.filter((fragment) => !identifiers.includes(fragment.identifier)) ?? [];
57
+ const { length } = toRemove;
58
+ for (let index = 0;index < length; index += 1) {
59
+ toRemove[index].remove();
60
+ }
61
+ return {
62
+ fragments: next,
63
+ nodes: next.flatMap((fragment) => fragment.get())
64
+ };
65
+ }
66
+ function setNodes(fragments, nodes, comment, text, next) {
9
67
  if (nodes == null) {
10
68
  if (comment.parentNode != null) {
11
69
  replaceNodes([comment], next);
@@ -15,42 +73,50 @@ function setNodes(nodes, comment, text, value) {
15
73
  } else {
16
74
  replaceNodes(nodes, next);
17
75
  }
76
+ removeFragments(fragments);
18
77
  return next;
19
78
  }
20
79
  function setReactiveNode(data, comment, reactive) {
21
80
  const item = data.items.find((item2) => item2.nodes.includes(comment));
22
81
  const text = new Text;
82
+ let fragments;
23
83
  let nodes;
24
84
  effect(() => {
25
85
  const value = reactive.get();
26
- const valueIsFragment = isFragment(value);
27
- if (valueIsFragment || isProperNode(value)) {
28
- nodes = setNodes(nodes, comment, text, value);
86
+ if (Array.isArray(value)) {
87
+ const result = setArray(fragments, nodes, comment, text, value);
88
+ fragments = typeof result === "boolean" ? undefined : result?.fragments;
89
+ nodes = typeof result === "boolean" ? result ? [text] : undefined : result?.nodes;
29
90
  } else {
30
- nodes = setText(nodes, comment, text, value) ? [text] : undefined;
91
+ const valueIsFragment = isFragment(value);
92
+ fragments = valueIsFragment ? [value] : undefined;
93
+ if (valueIsFragment || isProperNode(value)) {
94
+ nodes = setNodes(fragments, nodes, comment, text, createNodes(value));
95
+ } else {
96
+ nodes = setText(fragments, nodes, comment, text, value);
97
+ }
31
98
  }
32
99
  if (item != null) {
33
- item.fragment = valueIsFragment ? value : undefined;
100
+ item.fragments = fragments;
34
101
  item.nodes = [...nodes ?? [comment]];
35
102
  }
36
103
  });
37
104
  }
38
- function setText(nodes, comment, text, value) {
105
+ function setText(fragments, nodes, comment, text, value) {
39
106
  const isNullable = isNullableOrWhitespace(value);
40
107
  text.textContent = isNullable ? "" : getString(value);
108
+ let result = false;
41
109
  if (nodes != null) {
42
110
  replaceNodes(nodes, [isNullable ? comment : text]);
43
- return !isNullable;
44
- }
45
- if (isNullable && comment.parentNode == null) {
111
+ result = !isNullable;
112
+ } else if (isNullable && comment.parentNode == null) {
46
113
  text.replaceWith(comment);
47
- return false;
48
- }
49
- if (!isNullable && text.parentNode == null) {
114
+ } else if (!isNullable && text.parentNode == null) {
50
115
  comment.replaceWith(text);
51
- return true;
116
+ result = true;
52
117
  }
53
- return false;
118
+ removeFragments(fragments);
119
+ return result ? [text] : undefined;
54
120
  }
55
121
  export {
56
122
  setReactiveNode
package/package.json CHANGED
@@ -10,8 +10,8 @@
10
10
  },
11
11
  "devDependencies": {
12
12
  "@biomejs/biome": "^1.8.3",
13
- "@types/bun": "^1.1.6",
14
- "bun": "^1.1.24",
13
+ "@types/bun": "^1.1.8",
14
+ "bun": "^1.1.27",
15
15
  "dts-bundle-generator": "^9.5.1",
16
16
  "typescript": "^5.5.4"
17
17
  },
@@ -47,5 +47,5 @@
47
47
  },
48
48
  "type": "module",
49
49
  "types": "types/index.d.ts",
50
- "version": "0.9.0"
50
+ "version": "0.10.0"
51
51
  }
package/src/fragment.ts CHANGED
@@ -2,6 +2,7 @@ import {html} from '@oscarpalmer/toretto/html';
2
2
  import {parse} from './html';
3
3
  import type {FragmentData, ProperNode} from './models';
4
4
  import {mapNodes} from './node';
5
+ import {removeNodes} from './helpers/dom';
5
6
 
6
7
  export class Fragment {
7
8
  private readonly $fragment = true;
@@ -26,7 +27,7 @@ export class Fragment {
26
27
  /**
27
28
  * Gets a list of the fragment's nodes
28
29
  */
29
- get(): Node[] {
30
+ get(): ProperNode[] {
30
31
  if (this.data.items.length === 0) {
31
32
  const parsed = parse(this.data);
32
33
 
@@ -44,16 +45,12 @@ export class Fragment {
44
45
 
45
46
  mapNodes(
46
47
  this.data,
47
- this.data.items.flatMap(
48
- item => item.fragment?.get() ?? item.nodes,
49
- ) as ProperNode[],
48
+ this.data.items.flatMap(item => item.fragment?.get() ?? item.nodes),
50
49
  );
51
50
  }
52
51
 
53
52
  return [
54
- ...(this.data.items.flatMap(
55
- item => item.fragment?.get() ?? item.nodes,
56
- ) as ProperNode[]),
53
+ ...this.data.items.flatMap(item => item.fragment?.get() ?? item.nodes),
57
54
  ];
58
55
  }
59
56
 
@@ -64,13 +61,11 @@ export class Fragment {
64
61
  const {length} = this.data.items;
65
62
 
66
63
  for (let index = 0; index < length; index += 1) {
67
- const item = this.data.items[index];
64
+ const {fragment, nodes} = this.data.items[index];
68
65
 
69
- item.fragment?.remove();
66
+ fragment?.remove();
70
67
 
71
- for (const node of item.nodes) {
72
- node.remove();
73
- }
68
+ removeNodes(nodes);
74
69
  }
75
70
 
76
71
  this.data.items.splice(0, length);
@@ -1,6 +1,7 @@
1
1
  import {getString} from '@oscarpalmer/atoms/string';
2
2
  import type {ProperNode} from '../models';
3
- import {isFragment, isProperNode} from './index';
3
+ import {isFragment, isProperElement, isProperNode} from './index';
4
+ import {removeEvents} from '../node/event';
4
5
 
5
6
  export function createNodes(value: unknown): ProperNode[] {
6
7
  if (isFragment(value)) {
@@ -14,16 +15,40 @@ export function createNodes(value: unknown): ProperNode[] {
14
15
  return [new Text(getString(value))];
15
16
  }
16
17
 
18
+ export function removeNodes(nodes: ProperNode[]): void {
19
+ sanitiseNodes(nodes);
20
+
21
+ const {length} = nodes;
22
+
23
+ for (let index = 0; index < length; index += 1) {
24
+ nodes[index].remove();
25
+ }
26
+ }
27
+
17
28
  export function replaceNodes(from: ProperNode[], to: ProperNode[]): void {
18
29
  const {length} = from;
19
30
 
20
31
  for (let index = 0; index < length; index += 1) {
21
- const node = from[index];
22
-
23
32
  if (index === 0) {
24
- node.replaceWith(...to);
33
+ from[index].replaceWith(...to);
25
34
  } else {
26
- node.remove();
35
+ from[index].remove();
36
+ }
37
+ }
38
+ }
39
+
40
+ function sanitiseNodes(nodes: ProperNode[]): void {
41
+ const {length} = nodes;
42
+
43
+ for (let index = 0; index < length; index += 1) {
44
+ const node = nodes[index];
45
+
46
+ if (isProperElement(node)) {
47
+ removeEvents(node);
48
+ }
49
+
50
+ if (node.hasChildNodes()) {
51
+ sanitiseNodes([...node.childNodes] as ProperNode[]);
27
52
  }
28
53
  }
29
54
  }
@@ -1,4 +1,5 @@
1
- import {isReactive} from '@oscarpalmer/sentinel';
1
+ import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
+ import {computed, isReactive} from '@oscarpalmer/sentinel';
2
3
  import type {FragmentData, ProperElement} from '../../models';
3
4
  import {mapEvent} from '../event';
4
5
  import {setAttribute} from './value';
@@ -11,28 +12,35 @@ function getValue(data: FragmentData, original: string): unknown {
11
12
  return matches == null ? original : data.values[+matches[1]];
12
13
  }
13
14
 
14
- export function mapAttributes(data: FragmentData, element: ProperElement): void {
15
- const attributes = [...element.attributes];
16
- const {length} = attributes;
17
-
18
- for (let index = 0; index < length; index += 1) {
19
- const {name, value} = attributes[index];
20
-
21
- const actual = getValue(data, value);
22
-
23
- if (name.startsWith('@')) {
24
- mapEvent(element, name, actual);
25
- } else if (name.includes('.') || isReactive(actual)) {
26
- mapValue(element, name, actual);
27
- }
15
+ export function mapAttributes(
16
+ data: FragmentData,
17
+ element: ProperElement,
18
+ ): void {
19
+ const attributes = [...element.attributes];
20
+ const {length} = attributes;
21
+
22
+ for (let index = 0; index < length; index += 1) {
23
+ const {name, value} = attributes[index];
24
+
25
+ const actual = getValue(data, value);
26
+
27
+ if (name.startsWith('@')) {
28
+ mapEvent(element, name, actual);
29
+ } else if (
30
+ name.includes('.') ||
31
+ typeof value === 'function' ||
32
+ isReactive(actual)
33
+ ) {
34
+ mapValue(element, name, actual);
28
35
  }
29
36
  }
37
+ }
30
38
 
31
39
  function mapValue(element: ProperElement, name: string, value: unknown): void {
32
40
  switch (true) {
33
41
  case typeof value === 'function':
34
- mapValue(element, name, value());
35
- return;
42
+ setAttribute(element, name, computed(value as GenericCallback));
43
+ break;
36
44
 
37
45
  default:
38
46
  setAttribute(element, name, value);
@@ -7,8 +7,9 @@ import {
7
7
  } from '@oscarpalmer/toretto/attribute';
8
8
  import type {ProperElement} from '../../models';
9
9
 
10
- const classPattern = /^class\./;
11
- const stylePattern = /^style\./;
10
+ const classExpression = /^class\./;
11
+ const styleFullExpression = /^style\.([\w-]+)(?:\.([\w-]+))?$/;
12
+ const stylePrefixExpression = /^style\./;
12
13
 
13
14
  export function setAttribute(
14
15
  element: ProperElement,
@@ -18,11 +19,11 @@ export function setAttribute(
18
19
  element.removeAttribute(name);
19
20
 
20
21
  switch (true) {
21
- case classPattern.test(name):
22
+ case classExpression.test(name):
22
23
  setClasses(element, name, value);
23
24
  return;
24
25
 
25
- case stylePattern.test(name):
26
+ case stylePrefixExpression.test(name):
26
27
  setStyle(element, name, value);
27
28
  return;
28
29
 
@@ -38,7 +39,7 @@ function setClasses(
38
39
  value: unknown,
39
40
  ): void {
40
41
  function update(value: unknown): void {
41
- if (/^(|true)$/.test(getString(value))) {
42
+ if (value === true) {
42
43
  element.classList.add(...classes);
43
44
  } else {
44
45
  element.classList.remove(...classes);
@@ -68,18 +69,16 @@ function setStyle(element: ProperElement, name: string, value: unknown): void {
68
69
  }
69
70
  }
70
71
 
71
- const [, property, unit] = /^\w+\.([a-z-]+)(?:\.(\w+))?$/i.exec(name) ?? [];
72
+ const [, property, unit] = styleFullExpression.exec(name) ?? [];
72
73
 
73
- if (property == null) {
74
- return;
75
- }
76
-
77
- if (isReactive(value)) {
78
- effect(() => {
79
- update(value.get());
80
- });
81
- } else {
82
- update(value);
74
+ if (property != null) {
75
+ if (isReactive(value)) {
76
+ effect(() => {
77
+ update(value.get());
78
+ });
79
+ } else {
80
+ update(value);
81
+ }
83
82
  }
84
83
  }
85
84
 
@@ -111,9 +110,7 @@ function updateProperty(
111
110
  name: string,
112
111
  value: unknown,
113
112
  ): void {
114
- (element as unknown as PlainObject)[name] = /^(|true)$/.test(
115
- getString(value),
116
- );
113
+ (element as unknown as PlainObject)[name] = value === true;
117
114
  }
118
115
 
119
116
  function updateSelected(
package/src/node/event.ts CHANGED
@@ -1,5 +1,21 @@
1
1
  import type {ProperElement} from '../models';
2
2
 
3
+ const controllers = new WeakMap<ProperElement, AbortController>();
4
+
5
+ const eventNamePattern = /^@([\w-]+)(?::([a-z:]+))?$/i;
6
+
7
+ function getController(element: ProperElement): AbortController {
8
+ let controller = controllers.get(element);
9
+
10
+ if (controller == null) {
11
+ controller = new AbortController();
12
+
13
+ controllers.set(element, controller);
14
+ }
15
+
16
+ return controller;
17
+ }
18
+
3
19
  function getOptions(options: string): AddEventListenerOptions {
4
20
  const parts = options.split(':');
5
21
 
@@ -17,13 +33,19 @@ export function mapEvent(
17
33
  ): void {
18
34
  element.removeAttribute(name);
19
35
 
20
- const [, type, options] = /^@(\w+)(?::([a-z:]+))?$/.exec(name) ?? [];
36
+ const [, type, options] = eventNamePattern.exec(name) ?? [];
21
37
 
22
38
  if (typeof value === 'function' && type != null) {
23
- element.addEventListener(
24
- type,
25
- value as EventListener,
26
- getOptions(options ?? ''),
27
- );
39
+ element.addEventListener(type, value as EventListener, {
40
+ ...getOptions(options ?? ''),
41
+ signal: getController(element).signal,
42
+ });
43
+ }
44
+ }
45
+
46
+ export function removeEvents(element: ProperElement): void {
47
+ if (controllers.has(element)) {
48
+ controllers.get(element)?.abort();
49
+ controllers.delete(element);
28
50
  }
29
51
  }
package/src/node/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- import {isReactive} from '@oscarpalmer/sentinel';
1
+ import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
+ import {computed, isReactive} from '@oscarpalmer/sentinel';
2
3
  import {isFragment, isProperElement} from '../helpers';
3
4
  import {createNodes} from '../helpers/dom';
4
5
  import type {FragmentData, ProperNode} from '../models';
@@ -41,8 +42,8 @@ export function mapNodes(data: FragmentData, nodes: ProperNode[]): void {
41
42
  function mapValue(data: FragmentData, comment: Comment, value: unknown): void {
42
43
  switch (true) {
43
44
  case typeof value === 'function':
44
- mapValue(data, comment, value());
45
- return;
45
+ setReactiveNode(data, comment, computed(value as GenericCallback));
46
+ break;
46
47
 
47
48
  case isReactive(value):
48
49
  setReactiveNode(data, comment, value);
@@ -1,3 +1,4 @@
1
+ import type { ProperNode } from './models';
1
2
  export declare class Fragment {
2
3
  private readonly $fragment;
3
4
  private readonly data;
@@ -9,7 +10,7 @@ export declare class Fragment {
9
10
  /**
10
11
  * Gets a list of the fragment's nodes
11
12
  */
12
- get(): Node[];
13
+ get(): ProperNode[];
13
14
  /**
14
15
  * Removes the fragment from the DOM
15
16
  */
@@ -1,3 +1,4 @@
1
1
  import type { ProperNode } from '../models';
2
2
  export declare function createNodes(value: unknown): ProperNode[];
3
+ export declare function removeNodes(nodes: ProperNode[]): void;
3
4
  export declare function replaceNodes(from: ProperNode[], to: ProperNode[]): void;
package/types/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Generated by dts-bundle-generator v9.5.1
2
2
 
3
+ export type ProperNode = CharacterData | Element;
3
4
  export declare class Fragment {
4
5
  private readonly $fragment;
5
6
  private readonly data;
@@ -11,7 +12,7 @@ export declare class Fragment {
11
12
  /**
12
13
  * Gets a list of the fragment's nodes
13
14
  */
14
- get(): Node[];
15
+ get(): ProperNode[];
15
16
  /**
16
17
  * Removes the fragment from the DOM
17
18
  */
@@ -1,2 +1,3 @@
1
1
  import type { ProperElement } from '../models';
2
2
  export declare function mapEvent(element: ProperElement, name: string, value: unknown): void;
3
+ export declare function removeEvents(element: ProperElement): void;