@oscarpalmer/abydon 0.9.0 → 0.11.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.
@@ -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.11.0"
51
51
  }
package/src/fragment.ts CHANGED
@@ -1,12 +1,18 @@
1
+ import type {Key} from '@oscarpalmer/atoms/models';
1
2
  import {html} from '@oscarpalmer/toretto/html';
2
3
  import {parse} from './html';
3
4
  import type {FragmentData, ProperNode} from './models';
4
5
  import {mapNodes} from './node';
6
+ import {removeNodes} from './helpers/dom';
5
7
 
6
8
  export class Fragment {
7
9
  private readonly $fragment = true;
8
10
  private readonly data: FragmentData;
9
11
 
12
+ get identifier(): Key | undefined {
13
+ return this.data.identifier;
14
+ }
15
+
10
16
  constructor(strings: TemplateStringsArray, expressions: unknown[]) {
11
17
  this.data = {
12
18
  expressions,
@@ -26,7 +32,7 @@ export class Fragment {
26
32
  /**
27
33
  * Gets a list of the fragment's nodes
28
34
  */
29
- get(): Node[] {
35
+ get(): ProperNode[] {
30
36
  if (this.data.items.length === 0) {
31
37
  const parsed = parse(this.data);
32
38
 
@@ -45,18 +51,26 @@ export class Fragment {
45
51
  mapNodes(
46
52
  this.data,
47
53
  this.data.items.flatMap(
48
- item => item.fragment?.get() ?? item.nodes,
49
- ) as ProperNode[],
54
+ item =>
55
+ item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes,
56
+ ),
50
57
  );
51
58
  }
52
59
 
53
60
  return [
54
- ...(this.data.items.flatMap(
55
- item => item.fragment?.get() ?? item.nodes,
56
- ) as ProperNode[]),
61
+ ...this.data.items.flatMap(
62
+ item =>
63
+ item.fragments?.flatMap(fragment => fragment.get()) ?? item.nodes,
64
+ ),
57
65
  ];
58
66
  }
59
67
 
68
+ identify(identifier: Key): Fragment {
69
+ this.data.identifier = identifier;
70
+
71
+ return this;
72
+ }
73
+
60
74
  /**
61
75
  * Removes the fragment from the DOM
62
76
  */
@@ -64,13 +78,14 @@ export class Fragment {
64
78
  const {length} = this.data.items;
65
79
 
66
80
  for (let index = 0; index < length; index += 1) {
67
- const item = this.data.items[index];
68
-
69
- item.fragment?.remove();
81
+ const {fragments, nodes} = this.data.items[index];
82
+ const {length} = fragments ?? [];
70
83
 
71
- for (const node of item.nodes) {
72
- node.remove();
84
+ for (let index = 0; index < length; index += 1) {
85
+ fragments?.[index]?.remove();
73
86
  }
87
+
88
+ removeNodes(nodes);
74
89
  }
75
90
 
76
91
  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,38 @@ 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 {
29
+ from[0]?.replaceWith(...to);
30
+
18
31
  const {length} = from;
19
32
 
33
+ for (let index = 1; index < length; index += 1) {
34
+ from[index].remove();
35
+ }
36
+ }
37
+
38
+ function sanitiseNodes(nodes: ProperNode[]): void {
39
+ const {length} = nodes;
40
+
20
41
  for (let index = 0; index < length; index += 1) {
21
- const node = from[index];
42
+ const node = nodes[index];
43
+
44
+ if (isProperElement(node)) {
45
+ removeEvents(node);
46
+ }
22
47
 
23
- if (index === 0) {
24
- node.replaceWith(...to);
25
- } else {
26
- node.remove();
48
+ if (node.hasChildNodes()) {
49
+ sanitiseNodes([...node.childNodes] as ProperNode[]);
27
50
  }
28
51
  }
29
52
  }
@@ -1,6 +1,25 @@
1
1
  import type {Fragment} from '../fragment';
2
2
  import type {ProperElement, ProperNode} from '../models';
3
3
 
4
+ export function compareArrays(
5
+ first: unknown[],
6
+ second: unknown[],
7
+ ): 'added' | 'dissimilar' | 'removed' {
8
+ const firstIsLarger = first.length > second.length;
9
+ const from = firstIsLarger ? first : second;
10
+ const to = firstIsLarger ? second : first;
11
+
12
+ if (
13
+ !from
14
+ .filter(key => to.includes(key))
15
+ .every((key, index) => to[index] === key)
16
+ ) {
17
+ return 'dissimilar';
18
+ }
19
+
20
+ return firstIsLarger ? 'removed' : 'added';
21
+ }
22
+
4
23
  export function isFragment(value: unknown): value is Fragment {
5
24
  return (
6
25
  typeof value === 'object' &&
package/src/models.ts CHANGED
@@ -1,14 +1,16 @@
1
+ import type {Key} from '@oscarpalmer/atoms/models';
1
2
  import type {Fragment} from './fragment';
2
3
 
3
4
  export type FragmentData = {
4
5
  expressions: unknown[];
6
+ identifier?: Key;
5
7
  items: FragmentItem[];
6
8
  strings: TemplateStringsArray;
7
9
  values: unknown[];
8
10
  };
9
11
 
10
12
  export type FragmentItem = {
11
- fragment?: Fragment;
13
+ fragments?: Fragment[];
12
14
  nodes: ProperNode[];
13
15
  };
14
16
 
@@ -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);
@@ -63,7 +64,7 @@ function replaceComment(
63
64
  const nodes = createNodes(value);
64
65
 
65
66
  if (item != null) {
66
- item.fragment = isFragment(value) ? value : undefined;
67
+ item.fragments = isFragment(value) ? [value] : undefined;
67
68
  item.nodes = nodes;
68
69
  }
69
70